
Automating Mac Apps
- 130 installs
- 39 repo stars
- Updated January 14, 2026
- spillwavesolutions/automating-mac-apps-plugin
Use automating-mac-apps for development tasks
About
automating-mac-apps: A skill for development. This provides functionality for development workflows.
- automating-mac-apps
Automating Mac Apps by the numbers
- 130 all-time installs (skills.sh)
- Ranked #2,717 of 4,347 Backend & APIs skills by installs in the Skillselion catalog
- Data as of Aug 2, 2026 (Skillselion catalog sync)
npx skills add https://github.com/spillwavesolutions/automating-mac-apps-plugin --skill automating-mac-appsAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 130 |
|---|---|
| repo stars | ★ 39 |
| Last updated | January 14, 2026 |
| Repository | spillwavesolutions/automating-mac-apps-plugin ↗ |
What it does
Use automating-mac-apps for development tasks
Files
Automating macOS Apps (Apple Events, AppleScript, JXA)
Technology Status
JXA and AppleScript are legacy (last major updates: 2015-2016). Modern alternatives:
- PyXA: Active Python automation (see installation below)
- Shortcuts App: Visual workflow builder
- Swift/Objective-C: Production-ready automation
macOS Sequoia 15 Notes
Test scripts on target macOS due to:
- Stricter TCC permissions
- Enhanced Apple Events security
- Sandbox improvements
Core framing (use this mental model)
- Apple Events: The underlying inter-process communication (IPC) transport for macOS automation.
- AppleScript: The original DSL (Domain-Specific Language) for Apple Events: best for discovery, dictionaries, and quick prototypes.
- JXA (JavaScript for Automation): A JavaScript binding to the Apple Events layer: best for data handling, JSON, and maintainable logic.
- ObjC Bridge: JavaScript access to Objective-C frameworks (Foundation, AppKit) for advanced macOS capabilities beyond app dictionaries.
When to use which
- Use AppleScript for discovery, dictionary exploration, UI scripting, and quick one-offs.
- Use JXA for robust logic, JSON pipelines, integration with Python/Node, and long-lived automation.
- Hybrid pattern (recommended): discover in AppleScript, implement in JXA for production use.
When to use this skill
- Foundation for app-specific skills (Calendar, Notes, Mail, Keynote, Excel, Reminders).
- Run the automation warm-up scripts before first automation to surface macOS permission prompts.
- Use as the base reference for permissions, shell integration, and UI scripting fallbacks.
Workflow (default)
1) Identify target app + dictionary using Script Editor. 2) Prototype a minimal command that works.
- Example:
tell application "Finder" to get name of first item in desktop
3) Decide language using the rules above. 4) Harden: add error handling, timeouts, and permission checks.
- JXA:
try { ... } catch (e) { console.log('Error:', e.message); } - AppleScript:
try ... on error errMsg ... end try
5) Validate: run a read-only command and check outputs.
- Example:
osascript -e 'tell application "Finder" to get name of home' - Confirm: Output matches expected (e.g., user home folder name)
6) Integrate via osascript for CLI or pipeline use. 7) UI scripting fallback only when the dictionary is missing/incomplete.
- Example UI Script:
tell application "System Events" to click button 1 of window 1 of process "App" - Example JXA:
Application('System Events').processes.byName('App').windows[0].buttons[0].click()
Validation Checklist
- [ ] Automation/Accessibility permissions granted (System Settings > Privacy & Security)
- [ ] App is running:
Application("App").running()returns true - [ ] State checked before acting (e.g., folder exists)
- [ ] Dictionary method used (not UI scripting)
- [ ] Delays/retries added for UI operations
- [ ] Read-only test command succeeds
- [ ] Output matches expected values
Automation permission warm-up
- Use before first automation run or after macOS updates to surface prompts:
- All apps at once:
skills/automating-mac-apps/scripts/request_automation_permissions.sh(or.py). - Per-app:
skills/automating-<app>/scripts/set_up_<app>_automation.{sh,py}(calendar, notes, mail, keynote, excel, reminders). - Voice Memos (no dictionary):
skills/automating-voice-memos/scripts/set_up_voice_memos_automation.shto activate app + check data paths; enable Accessibility + consider Full Disk Access. - Run from the same host you intend to automate with (Terminal vs Python) so the correct app gets Automation approval.
- Each script runs read-only AppleScript calls (list accounts/calendars/folders, etc.) to request Terminal/Python control; click “Allow” when prompted.
Modern Python Alternatives to JXA
PyXA (Python for macOS Automation) - Preferred for new projects:
PyXA Features
- Active development (v0.2.3+), modern Python syntax
- App automation: Safari, Calendar, Reminders, Mail, Music
- UI scripting, clipboard, notifications, AppleScript integration
- Method chaining:
app.lists().reminders().title()
PyXA Installation {#pyxa-installation}
# Install PyXA
pip install mac-pyxa
# Or with pip3 explicitly
pip3 install mac-pyxa
# Requirements:
# - Python 3.10+ (check with: python3 --version)
# - macOS 12+ (Monterey or later recommended)
# - PyObjC is installed automatically as a dependency
# Verify installation
python3 -c "import PyXA; print(f'PyXA {PyXA.__version__} installed successfully')"Note: All app-specific skills in this plugin that show PyXA examples assume PyXA is installed. See this section for installation.
PyXA Example (Safari Automation)
import PyXA
# Launch Safari and navigate
safari = PyXA.Safari()
safari.activate()
safari.open_location("https://example.com")
# Get current tab URL
current_url = safari.current_tab.url
print(f"Current URL: {current_url}")PyXA Example (Reminders)
import PyXA
reminders = PyXA.Reminders()
work_list = reminders.lists().by_name("Work")
# Add new reminder
new_reminder = work_list.reminders().push({
"name": "Review PyXA documentation",
"body": "Explore modern macOS automation options"
})PyXA Official Resources:
- Documentation: https://skaplanofficial.github.io/PyXA/
- GitHub: https://github.com/SKaplanOfficial/PyXA
- Community: Active Discord and GitHub discussions
---
PyObjC (Python-Objective-C Bridge) - For Low-Level macOS Integration:
PyObjC Capabilities
- Direct Framework Access: AppKit, Foundation, and all macOS frameworks
- Apple Events: Send Apple Events via Scripting Bridge
- Script Execution: Run AppleScript or JXA from Python
- System APIs: Direct access to CalendarStore, AddressBook, SystemEvents
Installation
pip install pyobjc
# Installs bridges for major frameworksPyObjC Example (AppleScript Execution)
from Foundation import NSAppleScript
# Execute AppleScript from Python
script_source = '''
tell application "Safari"
return URL of current tab
end tell
'''
script = NSAppleScript.alloc().initWithSource_(script_source)
result, error = script.executeAndReturnError_(None)
if error:
print(f"Error: {error}")
else:
print(f"Current Safari URL: {result.stringValue()}")PyObjC Example (App Control via Scripting Bridge)
from ScriptingBridge import SBApplication
# Control Mail app
mail = SBApplication.applicationWithBundleIdentifier_("com.apple.Mail")
inbox = mail.inboxes()[0] # Access first inbox
# Get unread message count
unread_count = inbox.unreadCount()
print(f"Unread messages: {unread_count}")PyObjC Official Resources:
- Documentation: https://pyobjc.readthedocs.io
- Examples: Extensive GitHub repositories with code samples
---
JXA Status
JXA has no updates since 2016. Use PyXA for new projects when possible.
When Not to Use
- Cross-platform automation (use Selenium/Playwright for web)
- Full UI testing (use XCUITest or Appium)
- Environments blocking Automation/Accessibility permissions
- Non-macOS platforms
- Simple shell scripting tasks (use Bash directly)
Related Skills
- App-specific automation (create
automating-[app]skills as needed) ci-cd-tccfor advanced permission management in automated environmentsmastering-applescriptfor AppleScript-focused workflows
Security Best Practices
Permission Management:
- Request minimal required permissions to reduce security risks
- Use code signing for production scripts (Developer ID certificate)
- Store credentials securely (Keychain, not hardcoded)
- Validate all inputs to prevent injection attacks
Official Apple Security Guidance:
Output expectations
- Keep examples minimal and runnable.
- JSON Output: For CLI pipelines, use
JSON.stringify(result)in JXA. - Example:
console.log(JSON.stringify({files: files, count: files.length})) - Exit Codes: Ensure
osascriptexits with 0 for success, non-zero for failure.
What to load
Tier 1: Essentials (Start Here)
- JXA Syntax & Patterns:
automating-mac-apps/references/basics.md - AppleScript Basics:
automating-mac-apps/references/applescript-basics.md - Cookbook (Common Recipes):
automating-mac-apps/references/recipes.md
Tier 2: Advanced & Production
- JXA Cookbook (Condensed):
automating-mac-apps/references/cookbook.md - Performance Patterns:
automating-mac-apps/references/applescript-performance.md - CI/CD & Permissions:
automating-mac-apps/references/ci-cd-tcc.md - Shell Environment:
automating-mac-apps/references/shell-environment.md - UI Scripting Inspector:
automating-mac-apps/references/ui-scripting-inspector.md
Tier 3: Specialized & Reference
- PyXA Core API Reference (complete class/method docs):
automating-mac-apps/references/pyxa-core-api-reference.md - PyXA Basics:
automating-mac-apps/references/pyxa-basics.md(Modern Python automation fundamentals) - AppleScript → PyXA Conversion:
automating-mac-apps/references/applescript-to-pyxa-conversion.md(Migration guide with examples) - Translation Checklist (AppleScript → JXA):
automating-mac-apps/references/translation-checklist.md(Comprehensive guide with examples and pitfalls) - JXA Helpers Library:
automating-mac-apps/references/helpers.js whoseBatching Patterns:automating-mac-apps/references/whos-batching.md- Dictionary Strategies:
automating-mac-apps/references/dictionary-strategies.md - ASObjC Helpers:
automating-mac-apps/references/applescript-asobjc.md
Related Skills:
web-browser-automation: Complete browser automation guide (Chrome, Edge, Brave, Arc)
AppleScriptObjC (ASObjC) essentials
Use ASObjC when AppleScript needs regex, JSON, or fast file IO.
Enable ASObjC
use AppleScript version "2.4"
use framework "Foundation"
use scripting additionsRegex search
on regexFind(sourceText, pattern)
set str to current application's NSString's stringWithString:sourceText
set regex to current application's NSRegularExpression's regularExpressionWithPattern:pattern options:0 |error|:(missing value)
set matches to regex's matchesInString:str options:0 range:{location:0, |length|:str's |length|()}
set resultList to {}
repeat with aMatch in matches
set matchRange to aMatch's range()
set end of resultList to (str's substringWithRange:matchRange) as text
end repeat
return resultList
end regexFindJSON parse/stringify
on parseJSON(jsonStr)
set str to current application's NSString's stringWithString:jsonStr
set d to str's dataUsingEncoding:(current application's NSUTF8StringEncoding)
set {res, err} to current application's NSJSONSerialization's JSONObjectWithData:d options:0 |error|:(reference)
if res is missing value then error (err's localizedDescription() as text)
return res as list
end parseJSON
on stringifyJSON(asListOrRecord)
if class of asListOrRecord is record then
set cocoaObj to current application's NSDictionary's dictionaryWithDictionary:asListOrRecord
else
set cocoaObj to current application's NSArray's arrayWithArray:asListOrRecord
end if
set {d, err} to current application's NSJSONSerialization's dataWithJSONObject:cocoaObj options:0 |error|:(reference)
if d is missing value then error (err's localizedDescription() as text)
set str to current application's NSString's alloc()'s initWithData:d encoding:(current application's NSUTF8StringEncoding)
return str as text
end stringifyJSONAtomic file write
on writeTextToFile(theText, thePath)
set str to current application's NSString's stringWithString:theText
set pth to current application's NSString's stringWithString:thePath
set expPath to pth's stringByExpandingTildeInPath()
set {res, err} to str's writeToFile:expPath atomically:true encoding:(current application's NSUTF8StringEncoding) |error|:(reference)
if res is false then error (err's localizedDescription() as text)
end writeTextToFileAppleScript basics (DSL for Apple Events)
Contents
- Mental model
- Core patterns
- Dictionary discovery
- Control flow + errors
- Shell interop
- UI scripting (last resort)
- UI scripting reliability helpers
- AppleScript strengths
Mental model
tell application "App"sends Apple Events to a scriptable app.- Most results are references; ask for properties to get values.
- Dictionaries define the nouns (classes) and verbs (commands).
Core patterns
tell application "Safari"
set theURL to URL of current tab of front window
end tell
return theURLtell application "Finder"
set names to name of every file of desktop
end tell
return namesDictionary discovery
- Script Editor > File > Open Dictionary...
- Use dictionary terms exactly (class names, properties, commands).
Control flow + errors
try
tell application "Finder" to get name of startup disk
on error errMsg number errNum
log ("Error " & errNum & ": " & errMsg)
end tryShell interop
set dirPath to "/tmp"
set cmd to "ls -la " & quoted form of dirPath
set out to do shell script cmd
return outUI scripting (last resort)
tell application "System Events"
tell process "Safari"
click menu item "New Tab" of menu 1 of menu bar item "File" of menu bar 1
end tell
end tellUI scripting reliability helpers
on waitForProcess(procName, timeoutSeconds)
set endTime to (current date) + timeoutSeconds
tell application "System Events"
repeat until (exists process procName) or ((current date) > endTime)
delay 0.2
end repeat
if not (exists process procName) then
error "Timed out waiting for process " & procName
end if
end tell
end waitForProcesson waitForUIElement(uiRef, timeoutSeconds)
set endTime to (current date) + timeoutSeconds
repeat until (exists uiRef) or ((current date) > endTime)
delay 0.2
end repeat
if not (exists uiRef) then error "Timed out waiting for UI element"
end waitForUIElementTips:
- Always
activatethe target app and wait for its process. - Use
existschecks and shortdelayloops instead of fixed sleeps. - Prefer menu items and named buttons over index-based UI paths.
AppleScript strengths
- Fast discovery with Script Editor.
- Compact UI scripting patterns.
- Lots of legacy examples.
AppleScript Basics for Developers (condensed)
Scope
AppleScript is the discovery and prototyping DSL for Apple Events. Use it to explore app dictionaries, validate commands, and UI script when necessary.
When to use AppleScript
- Dictionary discovery in Script Editor.
- Rapid prototyping of app commands.
- UI scripting with System Events.
- Small one-off automation.
When to prefer JXA instead
- Data-heavy logic, JSON pipelines, maintainable scripts.
- Integration with Python/Node.
Tooling essentials
- Script Editor: dictionary, event log, result pane.
osascript: run inline or file-based scripts.
Core patterns
tell application "Safari"
set theURL to URL of current tab of front window
end tell
return theURLtell application "Finder"
set names to name of every file of desktop
end tell
return namesError handling
try
tell application "Finder" to get name of startup disk
on error errMsg number errNum
log ("Error " & errNum & ": " & errMsg)
end tryUI scripting reliability
- Always
activatethe target app. - Use
existschecks + shortdelayloops. - Prefer named UI elements over index paths.
AppleScript -> JXA workflow
1) Prototype in AppleScript. 2) Map dictionary terms to JXA access patterns. 3) Port to JXA and return JSON.
AppleScript performance notes
Use whose to push filtering into the app
tell application "Finder"
set oldImages to (every file of folder "Documents" of home whose name extension is "png" and modification date < ((current date) - 30 * days))
end tellAvoid large client-side loops when possible
- One large
whosequery is far faster than thousands of per-item property reads.
Faster list access with a script object
script ListContainer
property fastList : {}
end script
set ListContainer's fastList to bigList
repeat with i from 1 to count of ListContainer's fastList
set itemValue to item i of ListContainer's fastList
end repeatAppleScript timeouts and async patterns
Extend Apple Event timeout
with timeout of 1800 seconds
tell application "Xcode" to build workspace document 1
end timeoutFire-and-forget
ignoring application responses
tell application "Finder" to open application file "Adobe Photoshop.app"
end ignoringAppleScript to PyXA Conversion Guide
Table of Contents
1. Introduction 2. Installation and Setup 3. Basic Syntax Mapping 4. Object Model Differences 5. Error Handling Conversions 6. Application-Specific Conversions 7. Troubleshooting 8. Comprehensive Examples 9. Pattern Reference 10. Migration Strategies
1. Introduction
AppleScript has been macOS's primary automation language since 1993, but PyXA (Python for Apple Automation) offers modern Python-based alternatives. This guide focuses on migrating from legacy AppleScript workflows to contemporary PyXA approaches.
What is PyXA?
PyXA is a Python wrapper around Apple's Scripting Bridge framework that enables AppleScript-like control over macOS applications using Python syntax.
Why Migrate?
- Modern Python ecosystem
- Better error handling
- Cross-platform compatibility
- Rich Python libraries integration
- Improved maintainability
2. Installation and Setup
Installing PyXA
pip install mac-pyxaBasic Import
import PyXA3. Basic Syntax Mapping
Application References
AppleScript:
tell application "Safari"
-- commands
end tellPyXA:
app = PyXA.Application("Safari")
# commandsProperty Access
AppleScript:
name of window 1PyXA:
app.windows()[0].nameMethod Calls
AppleScript:
activatePyXA:
app.activate()4. Object Model Differences
Collections and Indexing
AppleScript: 1-based indexing
item 1 of windowsPyXA: 0-based indexing
app.windows()[0]List Operations
AppleScript:
set myList to {1, 2, 3}
set item 2 of myList to 5PyXA:
my_list = [1, 2, 3]
my_list[1] = 5Record/Dictionary Access
AppleScript:
foo of {foo: "bar", spam: "eggs"}PyXA:
data = {"foo": "bar", "spam": "eggs"}
data["foo"]5. Error Handling Conversions
Try-Catch Blocks
AppleScript:
try
-- risky code
on error errMsg number errNum
-- handle error
end tryPyXA:
try:
# risky code
except Exception as e:
# handle errorAppleScript-Specific Errors
AppleScript:
tell application "Finder"
if not (exists file "test.txt") then
error "File not found"
end if
end tellPyXA:
from PyXA import XAErrors
try:
file_path = "/path/to/test.txt"
if not os.path.exists(file_path):
raise XAErrors.AppleScriptError("File not found")
except XAErrors.AppleScriptError as e:
print(f"Error: {e}")6. Application-Specific Conversions
Finder Operations
AppleScript:
tell application "Finder"
set desktopFiles to files of desktop
end tellPyXA:
finder = PyXA.Application("Finder")
desktop_files = finder.desktop().files()System Events (UI Scripting)
AppleScript:
tell application "System Events"
tell process "Safari"
click button "Allow" of window 1
end tell
end tellPyXA:
system_events = PyXA.Application("System Events")
safari_process = system_events.processes().by_name("Safari")
safari_process.windows()[0].buttons().by_name("Allow").click()7. Troubleshooting
Common Issues
1. IndexError on Collections
- Problem:
app.windows()[0]fails when no windows exist - Solution: Check collection length first
windows = app.windows()
if len(windows) > 0:
front_window = windows[0]2. Application Not Scriptable
- Problem: Some apps don't support scripting
- Solution: Use UI scripting or AppleScript bridge
script = PyXA.AppleScript('tell application "App" to activate')
script.run()3. Permission Issues
- Problem: macOS security restrictions
- Solution: Grant accessibility permissions in System Settings
4. Type Conversion Issues
- Problem: PyXA returns different types than expected
- Solution: Check object types and convert explicitly
result = app.some_property
if isinstance(result, PyXA.XATypes.XAText):
text_value = str(result)5. Method Not Found
- Problem: PyXA method names differ from AppleScript
- Solution: Check PyXA documentation or use
dir()to explore available methods
Debugging Tips
- Use
print(type(obj))to check object types - Use
dir(obj)to see available methods/properties - Enable verbose error messages
- Test AppleScript portions separately first
8. Comprehensive Examples
Example 1: Safari Tab Management
Original AppleScript:
tell application "Safari"
set tabList to {}
repeat with t in tabs of window 1
set end of tabList to (name of t & " - " & URL of t)
end repeat
return tabList
end tellPyXA Conversion:
import PyXA
def get_safari_tabs():
"""Get list of Safari tab titles and URLs"""
try:
safari = PyXA.Application("Safari")
tabs = safari.windows()[0].tabs()
tab_list = []
for tab in tabs:
tab_info = f"{tab.name} - {tab.url}"
tab_list.append(tab_info)
return tab_list
except IndexError:
return ["No Safari windows open"]
except Exception as e:
return [f"Error: {str(e)}"]
# Usage
tabs = get_safari_tabs()
for tab in tabs:
print(tab)Example 2: File Organization Script
Original AppleScript:
tell application "Finder"
set sourceFolder to folder "Downloads" of home
set destFolder to folder "Documents" of home
set fileList to every file of sourceFolder whose name extension is "pdf"
repeat with aFile in fileList
move aFile to destFolder
end repeat
end tellPyXA Conversion:
import PyXA
import os
def organize_pdf_files():
"""Move PDF files from Downloads to Documents folder"""
try:
finder = PyXA.Application("Finder")
# Get source and destination folders
home_path = os.path.expanduser("~")
downloads_path = os.path.join(home_path, "Downloads")
documents_path = os.path.join(home_path, "Documents")
source_folder = finder.folders().by_path(downloads_path)
dest_folder = finder.folders().by_path(documents_path)
if not source_folder or not dest_folder:
raise ValueError("Source or destination folder not found")
# Find PDF files
pdf_files = []
for item in source_folder.items():
if hasattr(item, 'name_extension') and item.name_extension == "pdf":
pdf_files.append(item)
# Move files
moved_count = 0
for pdf_file in pdf_files:
pdf_file.move_to(dest_folder)
moved_count += 1
return f"Successfully moved {moved_count} PDF files"
except Exception as e:
return f"Error organizing files: {str(e)}"
# Usage
result = organize_pdf_files()
print(result)9. Pattern Reference (10 Examples)
1. Application Activation
AppleScript:
tell application "TextEdit" to activatePyXA:
PyXA.Application("TextEdit").activate()2. Getting Window Count
AppleScript:
tell application "Safari"
count of windows
end tellPyXA:
len(PyXA.Application("Safari").windows())3. Creating New Document
AppleScript:
tell application "TextEdit"
make new document
end tellPyXA:
PyXA.Application("TextEdit").new_document()4. Getting Selected Text
AppleScript:
tell application "TextEdit"
selected text of front document
end tellPyXA:
app = PyXA.Application("TextEdit")
app.front_document.selected_text5. File Existence Check
AppleScript:
tell application "Finder"
exists file "test.txt" of home
end tellPyXA:
import os
os.path.exists(os.path.expanduser("~/test.txt"))6. Folder Contents
AppleScript:
tell application "Finder"
every file of folder "Desktop" of home
end tellPyXA:
finder = PyXA.Application("Finder")
desktop = finder.desktop()
desktop.items()7. System Beep
AppleScript:
beepPyXA:
import subprocess
subprocess.run(["afplay", "/System/Library/Sounds/Basso.aiff"])8. Current Date/Time
AppleScript:
current datePyXA:
import datetime
datetime.datetime.now()9. String Concatenation
AppleScript:
"Hello " & "World"PyXA:
"Hello " + "World"10. List Operations
AppleScript:
set myList to {1, 2, 3}
set end of myList to 4PyXA:
my_list = [1, 2, 3]
my_list.append(4)10. Migration Strategies
Gradual Migration Approach
1. Identify Core Functionality: Start with simple, self-contained AppleScript functions 2. Create PyXA Equivalents: Convert one function at a time 3. Test Thoroughly: Ensure behavior matches exactly 4. Integrate Python Libraries: Leverage Python's ecosystem for enhanced functionality 5. Refactor for Maintainability: Use Python best practices
Key Benefits of Migration
- Type Safety: Python's type hints and modern IDE support
- Testing: Unit testing frameworks (pytest, unittest)
- Version Control: Better integration with git and modern development workflows
- Package Management: pip and requirements.txt for dependency management
- Cross-Platform Potential: Easier to adapt for non-macOS environments
Best Practices
- Keep original AppleScript as reference during conversion
- Add comprehensive error handling
- Use Python's logging instead of AppleScript's
logcommand - Leverage Python's data structures over AppleScript's limited types
- Consider async/await for long-running operations
This guide provides a foundation for migrating AppleScript automations to PyXA. The PyXA documentation at https://skaplanofficial.github.io/PyXA/ contains comprehensive API references for specific applications.
JXA basics (JavaScript binding for Apple Events)
Contents
- Mental model
- Core patterns
- Standard additions
- Shell interop
- Error handling
- Runtime notes
- Runtime quirks (practical)
- ObjC bridge (when you need it)
- JXA UI scripting (System Events)
- Dictionary discovery cheat sheet (AppleScript -> JXA)
Mental model
- Same Apple Events layer as AppleScript.
Application("App")maps to the app dictionary.- Best for JSON, data transforms, and maintainable logic.
Core patterns
const safari = Application("Safari");
const url = safari.windows[0].currentTab().url();
url;const finder = Application("Finder");
const names = finder.desktop.files().map(f => f.name());
JSON.stringify(names);Standard additions
const app = Application.currentApplication();
app.includeStandardAdditions = true;
app.displayDialog("Hello");Shell interop
const app = Application.currentApplication();
app.includeStandardAdditions = true;
const out = app.doShellScript("whoami");
out;Error handling
try {
const mail = Application("Mail");
const subjects = mail.selection().map(m => m.subject());
JSON.stringify(subjects);
} catch (err) {
throw new Error("Mail automation failed: " + err);
}Runtime notes
- JXA runs under JavaScriptCore (not Node.js).
- Avoid modern JS features that may not be supported.
- Prefer JSON output for pipelines.
Runtime quirks (practical)
console.logwrites to stderr inosascript; return values go to stdout.- Collections from apps are often special proxy objects; convert with
.mapwhen possible. delay(seconds)is a global provided by the automation environment.- Some apps require
app.activate()before accessing windows or UI state.
ObjC bridge (when you need it)
Use ObjC to access Cocoa APIs for file IO, clipboard, or dates.
ObjC.import("Foundation");
const str = $.NSString.alloc.initWithUTF8String("Hello");
const path = $.NSString.stringWithString("~/Desktop/jxa.txt")
.stringByExpandingTildeInPath;
str.writeToFileAtomicallyEncodingError(path, true, $.NSUTF8StringEncoding, null);JXA UI scripting (System Events)
const se = Application("System Events");
const safari = se.processes.byName("Safari");
Application("Safari").activate();
delay(0.2);
safari.menuBars[0].menuBarItems.byName("File").menus[0].menuItems.byName("New Tab").click();Dictionary discovery cheat sheet (AppleScript -> JXA)
Use Script Editor to learn terms, then translate with these patterns.
tell application "App" -> const app = Application("App");
current tab of front window -> app.windows[0].currentTab()
name of every file of desktop -> app.desktop.files().map(f => f.name())
every document -> app.documents()
make new document -> app.Document({}).make()
set name of x to "Y" -> x.name = "Y"Notes:
- Collections are often functions in JXA (
documents()), not properties. - Properties are accessed as methods (
doc.name()), but can be set directly (doc.name = "New").
JXA Calendar and Notes recipes
List upcoming calendar events
const calendar = Application("Calendar");
const cal = calendar.calendars.byName("Work");
const today = new Date();
const future = new Date();
future.setDate(future.getDate() + 7);
const events = cal.events.whose({ startDate: { ">": today, "<": future } });
const out = events.map(e => ({ title: e.title(), start: e.startDate().toISOString() }));
JSON.stringify({ count: out.length, events: out });Create a note
const notes = Application("Notes");
const folder = notes.folders.byName("Notes");
const note = notes.Note({ name: "New Note", body: "Hello" });
folder.notes.push(note);
JSON.stringify({ success: true });JXA Chrome recipes
Required setting
- In Chrome: View -> Developer -> Allow JavaScript from Apple Events
List tabs
const chrome = Application("Google Chrome");
const tabs = [];
chrome.windows().forEach(win => {
win.tabs().forEach(tab => tabs.push({ title: tab.title(), url: tab.url() }));
});
JSON.stringify({ count: tabs.length, tabs });CI/CD and TCC (safe guidance)
Core constraints
- Headless runners cannot approve Automation or Accessibility prompts.
- UI scripting is effectively blocked in CI unless pre-authorized.
- Prefer dictionary automation and shell commands in CI.
Safe practices
- Use dry-run mode first; emit JSON reports to stdout.
- Fail gracefully when permissions are missing.
- Use MDM profiles to pre-authorize Apple Events and Accessibility in enterprise environments.
Avoid (unsafe / brittle)
- Direct TCC database edits or sqlite injection.
- Clicking security prompts via UI scripting.
Headless check pattern (AppleScript)
try
tell application "System Events" to set uiEnabled to UI elements enabled
on error
-- Likely headless
end tryJXA Developer Cookbook (condensed)
Scope
For production macOS automation using JXA (JavaScript for Automation). Use this after AppleScript discovery, when you need maintainable logic, JSON output, and pipeline integration.
When to use JXA
- Automation logic that benefits from real JS (maps, filters, JSON).
- Pipelines where stdout must be structured JSON.
- Long-lived scripts with retries and defensive checks.
When not to use JXA
- Cross-platform automation without Apple Events.
- UI-heavy workflows that are better in Shortcuts.
- Headless CI where GUI apps are unavailable.
Runtime notes (practical)
- JXA runs under JavaScriptCore (not Node.js).
- Prefer synchronous patterns; async/await is unreliable.
- Use
JSON.stringify()for stdout;console.logwrites to stderr.
Core mental model
Application("App")is an Apple Events bridge to the app.- Collections are often functions:
app.documents(). - Properties are methods for reads:
doc.name(); setters are direct:doc.name = "New".
Helper library
Use the shared helper functions from automating_mac_apps/references/recipes.md (waitUntil, jsonOut, shQuote, safeGet). Add backoff and activation helpers when needed.
Recipes index (production-focused)
- Mail: create/send, export selection, archive by date.
- Finder: list metadata, batch rename, copy by type, cleanup old files.
- Pages: template replace, export PDF, batch export, search/replace.
- Safari/Chrome: list tabs, open URLs, close by pattern.
- Keynote: create presentation, export PDF.
- Calendar/Notes: list events, create events, create notes, export notes.
- UI scripting: System Events for non-scriptable apps.
- Pipelines: JXA -> Python/Node JSON.
- CI: dry-run patterns, headless constraints.
- Debugging: structured logging, introspection.
Recipe: JSON pipeline output (canonical)
function run() {
try {
const finder = Application("Finder");
const files = finder.desktop.files().map(f => ({ name: f.name() }));
return { success: true, files };
} catch (e) {
return { success: false, error: e.message };
}
}
JSON.stringify(run());Recipe: retry with backoff (JXA)
function retryWithBackoff(fn, maxRetries = 3, initialDelayMs = 100) {
let lastError;
for (let i = 0; i < maxRetries; i++) {
try { return fn(); } catch (e) {
lastError = e;
delay((initialDelayMs * Math.pow(2, i)) / 1000);
}
}
throw lastError;
}CI/headless guidance
- Avoid UI scripting; prefer dictionaries or shell commands.
- Use dry-run patterns; emit JSON reports for CI parsing.
- Many GUI apps require an interactive session; fail gracefully.
App dictionary exploration strategies
Use Script Editor dictionaries
- Script Editor -> File -> Open Dictionary...
- Start with classes (nouns) then commands (verbs).
Exploration workflow
1) Find the top-level collections (documents, windows, selection). 2) Identify properties (name, id, url, bounds). 3) Test one query at a time in Script Editor. 4) Capture stable identifiers (id) for longer workflows.
Rapid query patterns
tell application "Safari"
get name of front window
end telltell application "Mail"
get count of messages of mailbox "INBOX"
end tellMapping to JXA
- Use
automating_mac_apps/references/translation-checklist.md.
JXA Finder recipes
List desktop files
const finder = Application("Finder");
const files = finder.desktop.files().map(f => ({
name: f.name(),
kind: f.kind(),
size: f.physicalSize(),
}));
JSON.stringify({ count: files.length, files });Batch rename with prefix
const app = Application.currentApplication();
app.includeStandardAdditions = true;
const targetDir = "/tmp";
const oldPrefix = "old_";
const newPrefix = "new_";
const cmd = "cd " + shQuote(targetDir) + "; ls -1 " + shQuote(oldPrefix + "*");
const out = app.doShellScript(cmd).split("\n").filter(Boolean);
out.forEach(name => app.doShellScript("mv " + shQuote(name) + " " + shQuote(name.replace(oldPrefix, newPrefix))));
JSON.stringify({ renamed: out.length });// JXA helper library template (inline or concatenate)
// Keep this small and copyable into scripts.
function waitUntil(fn, timeoutSeconds = 5, stepSeconds = 0.2) {
const deadline = Date.now() + timeoutSeconds * 1000;
while (Date.now() < deadline) {
try { if (fn()) return true; } catch (_) {}
delay(stepSeconds);
}
throw new Error("Timeout in waitUntil");
}
function jsonOut(data) {
return JSON.stringify(data);
}
function shQuote(s) {
return "'" + String(s).replace(/'/g, "'\\''") + "'";
}
function retryWithBackoff(fn, maxRetries = 3, initialDelayMs = 100) {
let lastError;
for (let i = 0; i < maxRetries; i++) {
try { return fn(); } catch (e) {
lastError = e;
delay((initialDelayMs * Math.pow(2, i)) / 1000);
}
}
throw lastError;
}
function ensureActive(appName, timeoutSeconds = 10) {
const app = Application(appName);
app.activate();
const ok = waitUntil(() => {
try { return Application("System Events").processes.byName(appName).frontmost(); }
catch (_) { return false; }
}, timeoutSeconds);
if (!ok) throw new Error("Failed to activate " + appName);
return app;
}
JXA Keynote recipes
Export current presentation to PDF
const keynote = Application("Keynote");
const doc = keynote.documents[0];
if (!doc) throw new Error("No presentation open");
doc.export({ to: Path("/tmp/presentation.pdf"), as: "PDF" });
JSON.stringify({ success: true });JXA Mail recipes
Send email with attachments
const mail = Application("Mail");
const msg = mail.OutgoingMessage().make();
msg.visible = true;
msg.toRecipients.push(mail.Recipient({ address: "recipient@example.com" }));
msg.subject = "Report";
msg.content = "See attached.";
msg.attachments.push(mail.Attachment({ fileName: Path("/path/to/file.pdf") }));
msg.send();
JSON.stringify({ success: true });Export selected messages
const mail = Application("Mail");
const sel = mail.selection();
const out = sel.length
? sel.map(m => ({ subject: m.subject(), sender: m.sender() }))
: [];
JSON.stringify({ count: out.length, messages: out });Meeting Automation Playbook (cross-skill)
End-to-end steps covered by commands/agents:
1) Setup (`meeting-setup`)
- Parse attendees → upsert missing Contacts.
- Create prep reminders (deck, proposal, metrics) in Reminders with due dates before start.
- Update/create Calendar event; add alerts (1d/1h/15m).
2) Start (`meeting-start`)
- Detect in-progress meeting from Calendar by current time.
- Start Voice Memos recording named after the meeting.
3) End (`meeting-end`)
- Stop Voice Memos recording.
- Pull transcript (if available) from Voice Memos.
- Draft notes (agenda, decisions, action items) and a follow-up email to attendees.
- Create Reminders for action items.
Skill touchpoints:
- Contacts: attendee upsert/lookup.
- Reminders: prep tasks, follow-up tasks.
- Calendar: event lookup/create, multi-alerts.
- Voice Memos: start/stop recording, transcript scrape/save.
- Mail: draft follow-up email.
- Notes (forthcoming skill): file meeting notes under
meetings/<company>/<date>-<meeting-title>and people dossiers underpeople/<first>-<last>/.... - mac-apps: permissions (FDA/Accessibility), ObjC/SQLite helpers.
JXA Pages recipes
Export current document to PDF
const pages = Application("Pages");
const doc = pages.documents[0];
if (!doc) throw new Error("No document open");
doc.export({ to: Path("/tmp/export.pdf"), as: "PDF" });
JSON.stringify({ success: true });PyXA Basics (Python for macOS Automation)
Table of Contents
1. Mental Model 2. Core Patterns 3. Error Handling 4. Security 5. Performance 6. Comprehensive Examples 7. Brief Examples
Mental Model
PyXA (Python for macOS Automation) is a modern Python wrapper around Apple's Scripting Bridge framework, enabling AppleScript- and JXA-like control over macOS applications. Unlike legacy JXA, PyXA uses Pythonic syntax with snake_case conventions and command chaining.
Key Concepts:
- Application Objects: References to macOS apps (e.g.,
PyXA.Application("Safari")) - Object Hierarchy: Apps contain windows, documents, and other objects accessible via method chaining
- Properties vs Methods: Object attributes use dot notation, actions use method calls
- Lists: Bulk operations via
XAListsubclasses for efficient batch processing - UI Scripting: System Events access for non-scriptable app automation
Core Patterns
Application Control Pattern
import PyXA
app = PyXA.Application("Safari")
app.activate() # Bring app to foreground
app.front_window.current_tab.url # Access nested propertiesCommand Chaining Pattern
# Chain methods for concise operations
PyXA.Application("Notes").new_note("Title", "Content").show()List Operations Pattern
# Use XAList for bulk operations
notes = PyXA.Application("Notes").notes()
titles = notes.name() # Get all note titles at onceUI Scripting Pattern
# Access non-scriptable apps via System Events
window = PyXA.Application("AppName").front_window
buttons = window.buttons()
play_button = buttons.by_object_description("Play")
play_button.click()Error Handling
Basic Exception Handling
import PyXA
from PyXA.XAErrors import ApplicationNotFoundError
try:
safari = PyXA.Application("Safari")
url = safari.front_window.current_tab.url
print(f"Current URL: {url}")
except ApplicationNotFoundError:
print("Safari is not installed or running")
except Exception as e:
print(f"Unexpected error: {e}")App State Validation
def safe_get_safari_url():
try:
safari = PyXA.Application("Safari")
if not safari.running():
safari.launch()
# Wait for app to be ready
import time
time.sleep(2)
return safari.front_window.current_tab.url
except Exception as e:
print(f"Failed to get Safari URL: {e}")
return NonePermission Error Handling
try:
mail = PyXA.Application("Mail")
messages = mail.inboxes()[0].messages()
except Exception as e:
if "permission" in str(e).lower():
print("Automation permission denied. Check System Settings > Privacy & Security")
else:
print(f"Mail access error: {e}")Security
Permission Management
- Automation Permissions: Required for app control (System Settings > Privacy & Security > Automation)
- Accessibility Permissions: Needed for UI scripting (System Settings > Privacy & Security > Accessibility)
- Minimal Permissions: Request only necessary permissions to reduce attack surface
Input Validation
def safe_create_note(title, content):
# Validate inputs to prevent injection
if not isinstance(title, str) or not isinstance(content, str):
raise ValueError("Title and content must be strings")
# Sanitize content
safe_content = content.replace("<script>", "").replace("</script>", "")
try:
PyXA.Application("Notes").new_note(title, safe_content)
except Exception as e:
print(f"Failed to create note: {e}")Code Signing
- Sign scripts for production use with Developer ID certificate
- Use
codesigncommand for distribution - Avoid storing credentials in scripts
Performance
Use XAList for Bulk Operations
import PyXA
from timeit import timeit
# Inefficient: Individual operations
def slow_method():
notes = PyXA.Application("Notes").notes()
titles = []
for note in notes:
titles.append(note.name)
return titles
# Efficient: Bulk operations
def fast_method():
notes = PyXA.Application("Notes").notes()
return notes.name() # 56x faster for large lists
# Measure performance
slow_time = timeit(slow_method, number=10)
fast_time = timeit(fast_method, number=10)
print(f"Slow: {slow_time:.4f}s, Fast: {fast_time:.4f}s")Minimize Apple Events
# Bad: Multiple Apple Events
tab = PyXA.Application("Safari").front_window.current_tab
url = tab.url
name = tab.name
print(url, name)
# Good: Single Apple Event with stored reference
tab = PyXA.Application("Safari").front_window.current_tab
print(tab.url, tab.name)Cache References
# Cache app references for repeated use
safari = PyXA.Application("Safari")
def get_current_url():
return safari.front_window.current_tab.url # Reuses cached referenceComprehensive Examples
Example 1: Safari Tab Manager
#!/usr/bin/env python
import PyXA
class SafariTabManager:
def __init__(self):
try:
self.safari = PyXA.Application("Safari")
if not self.safari.running():
self.safari.launch()
except Exception as e:
print(f"Failed to initialize Safari: {e}")
self.safari = None
def get_all_tab_info(self):
"""Get URLs and titles of all tabs across all windows"""
if not self.safari:
return []
tab_info = []
try:
for window in self.safari.windows():
for tab in window.tabs():
tab_info.append({
'url': tab.url,
'title': tab.name,
'window_index': window.index
})
except Exception as e:
print(f"Error getting tab info: {e}")
return tab_info
def close_duplicate_tabs(self):
"""Close duplicate URLs, keeping the first occurrence"""
if not self.safari:
return 0
closed_count = 0
seen_urls = set()
try:
# Collect all tabs first to avoid modification during iteration
all_tabs = []
for window in self.safari.windows():
for tab in window.tabs():
all_tabs.append(tab)
# Close duplicates
for tab in all_tabs:
if tab.url in seen_urls:
tab.close()
closed_count += 1
else:
seen_urls.add(tab.url)
except Exception as e:
print(f"Error closing duplicates: {e}")
return closed_count
def create_session_note(self):
"""Save current tab session to Notes app"""
if not self.safari:
return
try:
tabs = self.get_all_tab_info()
content = "Safari Session:\n\n"
for i, tab in enumerate(tabs, 1):
content += f"{i}. [{tab['title']}]({tab['url']})\n"
PyXA.Application("Notes").new_note("Safari Session", content).show()
except Exception as e:
print(f"Error creating session note: {e}")
# Usage
manager = SafariTabManager()
print("All tabs:", manager.get_all_tab_info())
closed = manager.close_duplicate_tabs()
print(f"Closed {closed} duplicate tabs")
manager.create_session_note()Example 2: Mail Processor with Reminders Integration
#!/usr/bin/env python
import PyXA
from datetime import datetime, timedelta
class MailReminderProcessor:
def __init__(self):
try:
self.mail = PyXA.Application("Mail")
self.reminders = PyXA.Application("Reminders")
except Exception as e:
print(f"Failed to initialize apps: {e}")
self.mail = None
self.reminders = None
def process_unread_messages(self):
"""Create reminders for unread messages requiring follow-up"""
if not self.mail or not self.reminders:
return 0
reminder_count = 0
try:
# Get all unread messages
unread_messages = []
for account in self.mail.accounts():
for mailbox in account.mailboxes():
if "INBOX" in mailbox.name.upper():
unread_messages.extend(mailbox.messages().by_read_status(False))
for message in unread_messages[:10]: # Limit to 10 for performance
# Check if message needs follow-up
if self._needs_followup(message):
self._create_followup_reminder(message)
reminder_count += 1
except Exception as e:
print(f"Error processing messages: {e}")
return reminder_count
def _needs_followup(self, message):
"""Determine if message needs follow-up based on content analysis"""
subject = message.subject or ""
sender = message.sender or ""
# Simple heuristics - customize as needed
followup_keywords = ['follow up', 'action required', 'please respond', 'urgent']
important_senders = ['boss@company.com', 'client@domain.com']
subject_lower = subject.lower()
sender_lower = sender.lower()
return (
any(keyword in subject_lower for keyword in followup_keywords) or
any(important in sender_lower for important in important_senders)
)
def _create_followup_reminder(self, message):
"""Create a reminder for message follow-up"""
try:
subject = message.subject or "No Subject"
sender = message.sender or "Unknown Sender"
title = f"Follow up: {subject}"
notes = f"From: {sender}\nOriginal message: {message.id()}"
# Create reminder due in 2 days
due_date = datetime.now() + timedelta(days=2)
# Get or create "Follow-ups" list
followup_list = None
for list_obj in self.reminders.lists():
if list_obj.name == "Follow-ups":
followup_list = list_obj
break
if not followup_list:
followup_list = self.reminders.new_list("Follow-ups")
followup_list.reminders().push({
'name': title,
'body': notes,
'due_date': due_date
})
except Exception as e:
print(f"Error creating reminder for message {message.id()}: {e}")
# Usage
processor = MailReminderProcessor()
created = processor.process_unread_messages()
print(f"Created {created} follow-up reminders")Brief Examples
Basic App Control
# Launch and activate app
PyXA.Application("Calculator").activate()
# Check if app is running
running = PyXA.Application("Safari").running()
# Get frontmost app
front_app = PyXA.current_application()File Operations
# Create new TextEdit document
textedit = PyXA.Application("TextEdit")
doc = textedit.new_document("Hello World")
# Save document
doc.save_in(PyXA.Path("/Users/user/Desktop/hello.txt"))Finder Operations
finder = PyXA.Application("Finder")
# Get desktop files
desktop_files = finder.desktop.files()
# Create new folder
new_folder = finder.desktop.folders().push("New Folder")
# Move files
file_to_move = finder.desktop.files().by_name("old.txt")
file_to_move.move_to(new_folder)Calendar Events
calendar_app = PyXA.Application("Calendar")
# Create new event
event = calendar_app.calendars()[0].events().push({
'summary': 'Meeting',
'start_date': datetime(2024, 1, 15, 10, 0),
'end_date': datetime(2024, 1, 15, 11, 0)
})
# Get today's events
today_events = calendar_app.events().by_start_date(datetime.now().date())Clipboard Operations
# Copy text to clipboard
PyXA.XAClipboard().set_text("Hello from PyXA")
# Get clipboard content
clipboard_text = PyXA.XAClipboard().text
# Copy image
image = PyXA.XAImage("/path/to/image.jpg")
PyXA.XAClipboard().set_image(image)UI Scripting
# Click system dialog button
system_events = PyXA.Application("System Events")
dialog = system_events.processes().by_name("App Name").windows()[0]
ok_button = dialog.buttons().by_name("OK")
ok_button.click()Notifications
# Show notification
notification = PyXA.XANotification()
notification.title = "Task Complete"
notification.subtitle = "Your automation finished"
notification.body = "All items processed successfully"
notification.show()Speech Recognition
# Basic speech recognition
recognizer = PyXA.XASpeechRecognizer()
recognizer.start()
# Listen for specific commands
detector = PyXA.XACommandDetector()
detector.commands = ["open safari", "close window"]
detector.start()Image Processing
# Load and manipulate images
image = PyXA.XAImage("/path/to/photo.jpg")
resized = image.resize(800, 600)
flipped = resized.flip_horizontally()
flipped.save("/path/to/modified.jpg")Menu Bar Extras
# Add item to menu bar
menu_bar = PyXA.XAMenuBar()
menu_item = menu_bar.add_menu_item("PyXA Action")
menu_item.action = lambda: print("Menu clicked!")PyXA Core API Reference
Complete API reference for PyXA core modules: base classes, protocols, types, and error handling.
Version: 0.3.0 | Python: 3.10+ | Source: PyXA GitHub
Contents
- Module Overview
- Class Hierarchy
- XABase Module
- XAObject
- XAList
- XAPath
- XAColor
- XAURL
- XAText
- XAImage
- XASound
- XALocation
- XAClipboard
- XABaseScriptable Module
- XASBApplication
- XASBWindow
- XASBWindowList
- XASBPrintable
- XATypes Module
- XAProtocols Module
- XAErrors Module
- Quick Reference Tables
- See Also
---
Module Overview
| Module | Purpose |
|---|---|
PyXA.XABase | Core wrapper classes for macOS objects (files, colors, URLs, text, images) |
PyXA.XABaseScriptable | Base classes for scriptable applications and windows |
PyXA.XATypes | Named tuple types for geometric data (points, rectangles) |
PyXA.XAProtocols | Protocol definitions for capability interfaces |
PyXA.XAErrors | Custom exception types for PyXA operations |
---
Class Hierarchy
XAObject
├── XAPath
├── XAColor
├── XAURL
├── XAText
├── XAImage
├── XASound
├── XALocation
└── XASBObject
├── XASBApplication
│ └── [App-specific: XASafariApplication, XAMailApplication, etc.]
├── XASBWindow
└── XASBPrintable
XAList
├── XAPathList
├── XAURLList
├── XATextList
├── XAImageList
└── XASBWindowList
XAClipboard (standalone utility class)---
XABase Module
XAObject
Base class for all PyXA wrapper objects. Provides common functionality for macOS object manipulation.
class XAObject:
"""Base wrapper for macOS Scripting Bridge objects."""
# Initialization
def __init__(self, properties: dict = None) -> None
# Properties
@property
def xa_elem(self) -> Any # Underlying SBObject
@property
def xa_prnt(self) -> 'XAObject' # Parent object reference
# Methods
def set_property(self, property_name: str, value: Any) -> None
def has_element_with_properties(self, properties: dict) -> boolExample:
import PyXA
# All PyXA objects inherit from XAObject
safari = PyXA.Safari() # XASafariApplication extends XASBApplication extends XAObject
# Access underlying Scripting Bridge object
sb_object = safari.xa_elem---
XAList
Base class for PyXA list objects. Provides bulk operations and fast enumeration patterns.
class XAList(XAObject):
"""Wrapper for lists of scriptable objects with bulk operations."""
# Initialization
def __init__(self, properties: dict = None, filter: dict = None) -> None
# Properties
@property
def xa_ocls(self) -> type # Object class for list elements
# List Operations
def __len__(self) -> int
def __iter__(self) -> Iterator
def __getitem__(self, key: Union[int, slice]) -> Union[XAObject, 'XAList']
# Filtering
def by_property(self, property_name: str, value: Any) -> 'XAList'
def by_name(self, name: str) -> XAObject
def containing(self, property_name: str, value: str) -> 'XAList'
# Bulk Property Access (Fast Enumeration)
def name(self) -> list[str]
def id(self) -> list[Any]
# Transformations
def filter(self, predicate: Callable) -> 'XAList'
def first(self) -> XAObject
def last(self) -> XAObject
def at(self, index: int) -> XAObject
def push(self, element: Union[XAObject, dict]) -> XAObjectExample:
import PyXA
safari = PyXA.Safari()
tabs = safari.windows()[0].tabs() # XAList of tabs
# Bulk property access (fast)
all_titles = tabs.name() # Returns list of all tab names at once
# Filtering
github_tabs = tabs.containing("url", "github.com")
# Iteration
for tab in tabs:
print(tab.url())★ Insight ───────────────────────────────────── XAList's bulk property access methods (like .name(), .url()) are dramatically faster than iterating and calling properties individually. PyXA uses Scripting Bridge's fast enumeration under the hood, making a single Apple Event call for all values. ─────────────────────────────────────────────────
---
XAPath
Wrapper for file system paths with extensive file/folder operations.
class XAPath(XAObject):
"""Wrapper for file system paths."""
# Initialization
def __init__(self, path: Union[str, 'XAPath', NSURL]) -> None
# Properties
@property
def path(self) -> str # POSIX path string
@property
def url(self) -> NSURL # NSURL representation
@property
def name(self) -> str # File/folder name
@property
def extension(self) -> str # File extension
@property
def parent(self) -> 'XAPath' # Parent directory
@property
def exists(self) -> bool # Path exists on disk
@property
def is_file(self) -> bool
@property
def is_directory(self) -> bool
# File Operations
def open(self) -> None # Open with default app
def reveal_in_finder(self) -> None
def move_to(self, destination: 'XAPath') -> 'XAPath'
def copy_to(self, destination: 'XAPath') -> 'XAPath'
def delete(self) -> None # Move to Trash
# Content Operations
def read_text(self, encoding: str = 'utf-8') -> str
def write_text(self, content: str, encoding: str = 'utf-8') -> None
def read_data(self) -> bytes
def write_data(self, data: bytes) -> NoneExample:
import PyXA
# Create path object
doc = PyXA.XAPath("~/Documents/report.txt")
# Check properties
if doc.exists:
content = doc.read_text()
print(f"File: {doc.name}, Extension: {doc.extension}")
# Reveal in Finder
doc.reveal_in_finder()
# Parent directory
parent = doc.parent # ~/Documents as XAPath---
XAColor
Wrapper for color values with conversion between color spaces.
class XAColor(XAObject):
"""Wrapper for NSColor with color space conversions."""
# Class Methods (Preset Colors)
@classmethod
def white(cls) -> 'XAColor'
@classmethod
def black(cls) -> 'XAColor'
@classmethod
def red(cls) -> 'XAColor'
@classmethod
def green(cls) -> 'XAColor'
@classmethod
def blue(cls) -> 'XAColor'
@classmethod
def yellow(cls) -> 'XAColor'
@classmethod
def orange(cls) -> 'XAColor'
@classmethod
def purple(cls) -> 'XAColor'
@classmethod
def gray(cls) -> 'XAColor'
@classmethod
def clear(cls) -> 'XAColor' # Transparent
# Initialization
@classmethod
def from_hex(cls, hex_string: str) -> 'XAColor'
@classmethod
def from_rgb(cls, red: float, green: float, blue: float, alpha: float = 1.0) -> 'XAColor'
@classmethod
def from_hsb(cls, hue: float, saturation: float, brightness: float, alpha: float = 1.0) -> 'XAColor'
# Properties
@property
def hex_value(self) -> str # "#RRGGBB" format
@property
def red_value(self) -> float # 0.0-1.0
@property
def green_value(self) -> float
@property
def blue_value(self) -> float
@property
def alpha_value(self) -> float
@property
def hue_value(self) -> float # 0.0-1.0
@property
def saturation_value(self) -> float
@property
def brightness_value(self) -> float
# Methods
def make_swatch(self, width: int = 100, height: int = 100) -> 'XAImage'
def mix_with(self, color: 'XAColor', fraction: float = 0.5) -> 'XAColor'Example:
import PyXA
# Preset colors
bg_color = PyXA.XAColor.blue()
# From hex
accent = PyXA.XAColor.from_hex("#FF6B35")
# From RGB (values 0.0-1.0)
custom = PyXA.XAColor.from_rgb(0.5, 0.7, 0.9, 1.0)
# Convert to hex
print(f"Hex: {custom.hex_value}") # "#7FB2E5"
# Mix colors
blended = bg_color.mix_with(accent, 0.3)---
XAURL
Wrapper for URL handling with web and file URL support.
class XAURL(XAObject):
"""Wrapper for NSURL with URL parsing and manipulation."""
# Initialization
def __init__(self, url: Union[str, NSURL]) -> None
# Properties
@property
def url(self) -> str # Full URL string
@property
def scheme(self) -> str # http, https, file, etc.
@property
def host(self) -> str # Domain name
@property
def port(self) -> int
@property
def path(self) -> str # Path component
@property
def query(self) -> str # Query string
@property
def fragment(self) -> str # Fragment identifier
# Methods
def open(self) -> None # Open in default browser/app
def open_with(self, app: 'XASBApplication') -> NoneExample:
import PyXA
url = PyXA.XAURL("https://github.com/SKaplanOfficial/PyXA?tab=readme")
print(f"Host: {url.host}") # github.com
print(f"Path: {url.path}") # /SKaplanOfficial/PyXA
print(f"Query: {url.query}") # tab=readme
# Open in browser
url.open()---
XAText
Rich text wrapper with formatting and manipulation capabilities.
class XAText(XAObject):
"""Wrapper for NSAttributedString with rich text operations."""
# Initialization
def __init__(self, text: Union[str, NSAttributedString] = "") -> None
# Properties
@property
def text(self) -> str # Plain text content
@property
def length(self) -> int
# Text Manipulation
def words(self) -> list[str]
def sentences(self) -> list[str]
def paragraphs(self) -> list[str]
def replace(self, old: str, new: str) -> 'XAText'
def uppercase(self) -> 'XAText'
def lowercase(self) -> 'XAText'
def capitalize(self) -> 'XAText'
# Rich Text Operations
def set_font(self, font_name: str, size: float = 12.0) -> 'XAText'
def set_color(self, color: XAColor) -> 'XAText'
def bold(self) -> 'XAText'
def italic(self) -> 'XAText'
def underline(self) -> 'XAText'
# Clipboard
def copy_to_clipboard(self) -> NoneExample:
import PyXA
text = PyXA.XAText("Hello World! This is PyXA.")
# Analysis
words = text.words() # ['Hello', 'World!', 'This', 'is', 'PyXA.']
sentences = text.sentences() # ['Hello World!', 'This is PyXA.']
# Transformation
upper = text.uppercase() # "HELLO WORLD! THIS IS PYXA."
# Styling
styled = text.set_font("Helvetica", 18).bold().set_color(PyXA.XAColor.blue())
styled.copy_to_clipboard()---
XAImage
Image wrapper with processing and export capabilities.
class XAImage(XAObject):
"""Wrapper for NSImage with image processing."""
# Initialization
def __init__(self, image: Union[str, 'XAPath', NSImage, bytes]) -> None
@classmethod
def from_clipboard(cls) -> 'XAImage'
@classmethod
def from_url(cls, url: Union[str, XAURL]) -> 'XAImage'
@classmethod
def from_screen(cls, region: tuple = None) -> 'XAImage' # Screenshot
# Properties
@property
def size(self) -> tuple[int, int] # (width, height)
@property
def width(self) -> int
@property
def height(self) -> int
# Transformations
def resize(self, width: int, height: int) -> 'XAImage'
def scale(self, factor: float) -> 'XAImage'
def crop(self, x: int, y: int, width: int, height: int) -> 'XAImage'
def rotate(self, degrees: float) -> 'XAImage'
def flip_horizontal(self) -> 'XAImage'
def flip_vertical(self) -> 'XAImage'
# Filters
def grayscale(self) -> 'XAImage'
def sepia(self) -> 'XAImage'
def invert(self) -> 'XAImage'
def blur(self, radius: float = 10) -> 'XAImage'
def sharpen(self) -> 'XAImage'
# Export
def save(self, path: Union[str, XAPath], format: str = 'png') -> XAPath
def copy_to_clipboard(self) -> None
def show_in_preview(self) -> None
# OCR (Vision framework)
def extract_text(self) -> strExample:
import PyXA
# Load image
img = PyXA.XAImage("~/Pictures/photo.png")
# Transform
processed = (img
.resize(800, 600)
.grayscale()
.blur(5)
)
# Save
processed.save("~/Desktop/processed.png")
# Screenshot
screenshot = PyXA.XAImage.from_screen()
text = screenshot.extract_text() # OCR---
XASound
Audio playback and manipulation wrapper.
class XASound(XAObject):
"""Wrapper for NSSound with audio playback."""
# Initialization
def __init__(self, sound: Union[str, XAPath, NSSound]) -> None
@classmethod
def from_name(cls, name: str) -> 'XASound' # System sounds
# Properties
@property
def name(self) -> str
@property
def duration(self) -> float # Seconds
@property
def volume(self) -> float # 0.0-1.0
@volume.setter
def volume(self, value: float) -> None
@property
def is_playing(self) -> bool
# Playback
def play(self) -> None
def pause(self) -> None
def stop(self) -> None
def resume(self) -> NoneExample:
import PyXA
# System sound
beep = PyXA.XASound.from_name("Ping")
beep.play()
# Custom sound
music = PyXA.XASound("~/Music/track.mp3")
music.volume = 0.5
music.play()---
XALocation
Geolocation wrapper for coordinate handling.
class XALocation(XAObject):
"""Wrapper for CLLocation with coordinate operations."""
# Initialization
def __init__(self, latitude: float, longitude: float) -> None
@classmethod
def current_location(cls) -> 'XALocation' # Requires location permissions
# Properties
@property
def latitude(self) -> float
@property
def longitude(self) -> float
@property
def coordinates(self) -> tuple[float, float]
# Methods
def distance_to(self, other: 'XALocation') -> float # Meters
def reverse_geocode(self) -> dict # Address components
def open_in_maps(self) -> NoneExample:
import PyXA
# Create location
office = PyXA.XALocation(37.7749, -122.4194) # San Francisco
# Current location (requires permission)
current = PyXA.XALocation.current_location()
# Calculate distance
distance = current.distance_to(office)
print(f"Distance: {distance / 1000:.1f} km")
# Reverse geocode
address = office.reverse_geocode()
print(f"City: {address.get('city')}")---
XAClipboard
System clipboard access utility class.
class XAClipboard:
"""Utility class for system clipboard operations."""
# Class Methods
@classmethod
def get_text(cls) -> str
@classmethod
def set_text(cls, text: str) -> None
@classmethod
def get_image(cls) -> XAImage
@classmethod
def set_image(cls, image: XAImage) -> None
@classmethod
def get_urls(cls) -> list[XAURL]
@classmethod
def set_urls(cls, urls: list[XAURL]) -> None
@classmethod
def get_files(cls) -> list[XAPath]
@classmethod
def set_files(cls, files: list[XAPath]) -> None
@classmethod
def clear(cls) -> None
@classmethod
def content_types(cls) -> list[str] # Available types on clipboardExample:
import PyXA
# Text operations
PyXA.XAClipboard.set_text("Hello from PyXA!")
text = PyXA.XAClipboard.get_text()
# Image operations
screenshot = PyXA.XAImage.from_screen()
PyXA.XAClipboard.set_image(screenshot)
# Check available types
types = PyXA.XAClipboard.content_types()
if 'public.url' in types:
urls = PyXA.XAClipboard.get_urls()---
XABaseScriptable Module
XASBApplication
Base class for scriptable application wrappers.
class XASBApplication(XAObject):
"""Base class for scriptable applications."""
# Initialization
def __init__(self, properties: dict = None) -> None
# Properties
@property
def name(self) -> str # Application name
@property
def bundle_identifier(self) -> str # e.g., "com.apple.Safari"
@property
def version(self) -> str
@property
def frontmost(self) -> bool # Is active application
@property
def visible(self) -> bool
@visible.setter
def visible(self, value: bool) -> None
# State
def running(self) -> bool
# Actions
def activate(self) -> None # Bring to front
def hide(self) -> None
def quit(self, saving: str = 'yes') -> None # 'yes', 'no', 'ask'
# Windows
def windows(self, filter: dict = None) -> XASBWindowList
def front_window(self) -> XASBWindow
# Menu bar (UI scripting)
def menu_bar(self, index: int = 0) -> 'XAUIElement'Example:
import PyXA
# Get Safari application
safari = PyXA.Safari() # Inherits from XASBApplication
# Check state
if not safari.running():
safari.activate()
# Bring to front
safari.activate()
# Access windows
windows = safari.windows()
front = safari.front_window()---
XASBWindow
Base class for application window wrappers.
class XASBWindow(XAObject):
"""Base class for application windows."""
# Properties
@property
def name(self) -> str # Window title
@property
def id(self) -> int # Unique window ID
@property
def index(self) -> int # Window stack order
@index.setter
def index(self, value: int) -> None
@property
def bounds(self) -> tuple[int, int, int, int] # (x, y, width, height)
@bounds.setter
def bounds(self, value: tuple) -> None
@property
def position(self) -> tuple[int, int] # (x, y)
@position.setter
def position(self, value: tuple) -> None
@property
def size(self) -> tuple[int, int] # (width, height)
@size.setter
def size(self, value: tuple) -> None
@property
def visible(self) -> bool
@visible.setter
def visible(self, value: bool) -> None
@property
def miniaturized(self) -> bool # Minimized to dock
@miniaturized.setter
def miniaturized(self, value: bool) -> None
@property
def zoomed(self) -> bool # Maximized
@zoomed.setter
def zoomed(self, value: bool) -> None
# Actions
def close(self) -> NoneExample:
import PyXA
safari = PyXA.Safari()
window = safari.front_window()
# Get window info
print(f"Title: {window.name()}")
print(f"Position: {window.position}")
print(f"Size: {window.size}")
# Manipulate window
window.position = (100, 100)
window.size = (1200, 800)
window.zoomed = True # Maximize---
XASBWindowList
List wrapper for application windows with bulk operations.
class XASBWindowList(XAList):
"""List of application windows."""
# Bulk Property Access
def name(self) -> list[str]
def id(self) -> list[int]
def index(self) -> list[int]
def bounds(self) -> list[tuple]
def visible(self) -> list[bool]
def miniaturized(self) -> list[bool]
# Filtering
def by_name(self, name: str) -> XASBWindow
def by_id(self, id: int) -> XASBWindow
# Actions
def close(self) -> None # Close all windows---
XASBPrintable
Mixin protocol for objects that support printing.
class XASBPrintable:
"""Mixin for printable objects."""
def print(self,
print_dialog: bool = True,
copies: int = 1,
collating: bool = True,
pages_across: int = 1,
pages_down: int = 1) -> None---
XATypes Module
Named tuple types for geometric and temporal data.
from typing import NamedTuple
class XAPoint(NamedTuple):
"""2D coordinate point."""
x: float
y: float
class XARectangle(NamedTuple):
"""Rectangle defined by origin and size."""
x: float
y: float
width: float
height: float
class XADatetimeBlock(NamedTuple):
"""Date/time range for scheduling."""
start_date: datetime
end_date: datetimeExample:
from PyXA import XAPoint, XARectangle, XADatetimeBlock
from datetime import datetime, timedelta
# Point
position = XAPoint(100, 200)
print(f"X: {position.x}, Y: {position.y}")
# Rectangle
bounds = XARectangle(0, 0, 1920, 1080)
area = bounds.width * bounds.height
# Date block
meeting = XADatetimeBlock(
start_date=datetime.now(),
end_date=datetime.now() + timedelta(hours=1)
)---
XAProtocols Module
Protocol interfaces defining object capabilities.
Protocol Summary
| Protocol | Purpose | Key Methods |
|---|---|---|
XAProtocol | Base marker protocol | - |
XACanOpenPath | Can open file paths | open(path) |
XACanPrintPath | Can print file paths | print(path) |
XAClipboardCodable | Clipboard copy/paste | copy_to_clipboard() |
XACloseable | Can be closed | close() |
XADeletable | Can be deleted | delete() |
XAImageLike | Image conversion | to_image() -> XAImage |
XAPathLike | Path conversion | to_path() -> XAPath |
XAPrintable | Printing support | print() |
XASelectable | Selection support | select() |
XAShowable | Display/reveal | show() |
Protocol Definitions
from typing import Protocol
class XAClipboardCodable(Protocol):
"""Protocol for clipboard-compatible objects."""
def copy_to_clipboard(self) -> None: ...
class XACloseable(Protocol):
"""Protocol for closeable objects."""
def close(self, saving: str = 'yes') -> None: ...
class XADeletable(Protocol):
"""Protocol for deletable objects."""
def delete(self) -> None: ...
class XAImageLike(Protocol):
"""Protocol for objects convertible to images."""
def to_image(self) -> 'XAImage': ...
class XAPathLike(Protocol):
"""Protocol for objects with file paths."""
def to_path(self) -> 'XAPath': ...
@property
def path(self) -> str: ...
class XAPrintable(Protocol):
"""Protocol for printable objects."""
def print(self, **options) -> None: ...
class XASelectable(Protocol):
"""Protocol for selectable objects."""
def select(self) -> None: ...
class XAShowable(Protocol):
"""Protocol for objects that can be revealed/shown."""
def show(self) -> None: ...---
XAErrors Module
Custom exception types for PyXA operations.
Exception Hierarchy
Exception
└── PyXAError (base)
├── AppleScriptError
├── ApplicationNotFoundError
├── AuthenticationError
├── InvalidPredicateError
└── UnconstructableClassErrorException Definitions
class AppleScriptError(Exception):
"""Error executing AppleScript code.
Attributes:
script: The AppleScript that failed
error_message: Description from AppleScript engine
error_number: AppleScript error code
"""
def __init__(self, script: str, error_message: str, error_number: int = -1):
self.script = script
self.error_message = error_message
self.error_number = error_number
class ApplicationNotFoundError(Exception):
"""Application not installed or cannot be found.
Attributes:
app_name: Name of the application
bundle_id: Bundle identifier if provided
"""
def __init__(self, app_name: str, bundle_id: str = None):
self.app_name = app_name
self.bundle_id = bundle_id
class AuthenticationError(Exception):
"""Permission denied or authentication required.
Attributes:
app_name: Application requiring permission
permission_type: Type of permission needed (Automation, Accessibility, etc.)
"""
def __init__(self, app_name: str, permission_type: str):
self.app_name = app_name
self.permission_type = permission_type
class InvalidPredicateError(Exception):
"""Invalid filter predicate or query.
Attributes:
predicate: The invalid predicate string
reason: Why it's invalid
"""
def __init__(self, predicate: str, reason: str):
self.predicate = predicate
self.reason = reason
class UnconstructableClassError(Exception):
"""Cannot instantiate the requested class.
Attributes:
class_name: Name of the class
reason: Why construction failed
"""
def __init__(self, class_name: str, reason: str):
self.class_name = class_name
self.reason = reasonError Handling Example
import PyXA
from PyXA.XAErrors import (
ApplicationNotFoundError,
AuthenticationError,
AppleScriptError
)
try:
# Attempt to automate an app
mail = PyXA.Mail()
inbox = mail.inboxes()[0]
messages = inbox.messages()
except ApplicationNotFoundError as e:
print(f"App not found: {e.app_name}")
print("Please install the application and try again.")
except AuthenticationError as e:
print(f"Permission denied for {e.app_name}")
print(f"Grant '{e.permission_type}' permission in System Settings")
print("System Settings > Privacy & Security > Automation")
except AppleScriptError as e:
print(f"Script error ({e.error_number}): {e.error_message}")
print(f"Failed script: {e.script[:100]}...")---
Quick Reference Tables
Common Object Patterns
| Task | Code |
|---|---|
| Get application | app = PyXA.Safari() |
| Check if running | app.running() |
| Activate app | app.activate() |
| Get front window | app.front_window() |
| List all windows | app.windows() |
| Get window by name | app.windows().by_name("Title") |
Clipboard Operations
| Task | Code |
|---|---|
| Get text | PyXA.XAClipboard.get_text() |
| Set text | PyXA.XAClipboard.set_text("Hello") |
| Get image | PyXA.XAClipboard.get_image() |
| Set image | PyXA.XAClipboard.set_image(img) |
| Get files | PyXA.XAClipboard.get_files() |
| Clear | PyXA.XAClipboard.clear() |
Color Operations
| Task | Code |
|---|---|
| Preset color | PyXA.XAColor.blue() |
| From hex | PyXA.XAColor.from_hex("#FF6B35") |
| From RGB | PyXA.XAColor.from_rgb(1.0, 0.5, 0.2) |
| To hex | color.hex_value |
| Mix colors | color1.mix_with(color2, 0.5) |
File Operations
| Task | Code |
|---|---|
| Create path | PyXA.XAPath("~/Documents/file.txt") |
| Check exists | path.exists |
| Read text | path.read_text() |
| Write text | path.write_text("content") |
| Open file | path.open() |
| Reveal in Finder | path.reveal_in_finder() |
Image Operations
| Task | Code |
|---|---|
| Load image | PyXA.XAImage("path/to/image.png") |
| Screenshot | PyXA.XAImage.from_screen() |
| From clipboard | PyXA.XAImage.from_clipboard() |
| Resize | img.resize(800, 600) |
| Grayscale | img.grayscale() |
| Save | img.save("output.png") |
| OCR | img.extract_text() |
---
See Also
- Official PyXA Documentation: https://skaplanofficial.github.io/PyXA/
- PyXA GitHub: https://github.com/SKaplanOfficial/PyXA
- PyXA Basics Tutorial:
automating-mac-apps/references/pyxa-basics.md - AppleScript to PyXA Migration:
automating-mac-apps/references/applescript-to-pyxa-conversion.md - App-Specific PyXA References:
- Calendar:
automating-calendar/references/calendar-pyxa-api-reference.md - Contacts:
automating-contacts/references/contacts-pyxa-api-reference.md - Keynote:
automating-keynote/references/keynote-pyxa-api-reference.md - Mail:
automating-mail/references/mail-pyxa-api-reference.md - Notes:
automating-notes/references/notes-pyxa-api-reference.md - Numbers:
automating-numbers/references/numbers-pyxa-api-reference.md - Reminders:
automating-reminders/references/reminders-pyxa-api-reference.md
Recipes and side-by-side patterns
Contents
- AppleScript vs JXA: same Apple Event, different syntax
- Finder selection names
- Hybrid workflow (recommended)
- Pipeline pattern (Python + JXA)
- Helper library (JXA)
- Helper library (AppleScript)
- Worked example: AppleScript discovery -> JXA production
- Validation pattern
AppleScript vs JXA: same Apple Event, different syntax
Safari current tab URL
tell application "Safari"
set theURL to URL of current tab of front window
end tell
return theURLconst safari = Application("Safari");
safari.windows[0].currentTab().url();Finder selection names
tell application "Finder"
set sel to selection
if sel is {} then return "No selection"
return name of sel
end tellconst finder = Application("Finder");
const sel = finder.selection();
const out = sel.length
? { names: sel.map(f => f.name()) }
: { error: "No selection" };
JSON.stringify(out);Hybrid workflow (recommended)
1) Explore dictionary in Script Editor (AppleScript terms). 2) Prototype in AppleScript for quick discovery. 3) Port to JXA for production logic and JSON output.
Pipeline pattern (Python + JXA)
- Python orchestrates.
- JXA handles app automation and returns JSON.
Helper library (JXA)
Use the reusable helper template in automating_mac_apps/references/jxa-helpers.js.
Helper library (AppleScript)
on safeGet(taskScript, fallback)
try
return taskScript's run()
on error
return fallback
end try
end safeGet
script ReadStartupDisk
on run()
tell application "Finder" to get name of startup disk
end run
end script
-- Example usage:
-- set result to safeGet(ReadStartupDisk, "Unknown")Worked example: AppleScript discovery -> JXA production
Step 1: AppleScript prototype (discover terms)
tell application "Finder"
if selection is {} then return "No selection"
set selNames to name of selection
end tell
return selNamesStep 2: Port to JXA (production + JSON)
const finder = Application("Finder");
const sel = finder.selection();
const out = sel.length
? { names: sel.map(f => f.name()) }
: { error: "No selection" };
JSON.stringify(out);Step 3: Use from Python (pipeline)
import json, subprocess
out = subprocess.check_output(
["osascript", "-l", "JavaScript", "get_finder_selection.js"],
text=True
)
data = json.loads(out)Validation pattern
Use a read-only probe before destructive automation.
const finder = Application("Finder");
const count = finder.selection().length;
JSON.stringify({ selectionCount: count });JXA Safari recipes
List all tabs
const safari = Application("Safari");
const tabs = [];
safari.windows().forEach(win => {
win.tabs().forEach(tab => tabs.push({ title: tab.name(), url: tab.url() }));
});
JSON.stringify({ count: tabs.length, tabs });Automation security prompts (TCC overview)
What triggers prompts
- Apple Events between apps (Automation permission).
- UI scripting via System Events (Accessibility permission).
Practical guidance
- Expect a one-time prompt per runner app (Terminal, Script Editor, .app).
- Approve prompts during interactive development.
- In CI/headless, assume prompts cannot be approved.
Safe handling
- Detect failures and return clear errors.
- Provide a fallback path or exit early.
- Use MDM profiles for enterprise automation.
Shell integration pitfalls (AppleScript)
PATH is minimal in do shell script
/bin/shnon-interactive, non-login.- Default PATH:
/usr/bin:/bin:/usr/sbin:/sbin.
Fixes
- Use absolute paths (most reliable).
- Or load system PATH helper:
do shell script "eval $(/usr/libexec/path_helper -s); git status"Quoting rule
- Always use
quoted form offor variables.
Tooling and workflow (VS Code + build tasks)
Source format
- Keep scripts as
.applescript(text) in git. - Compile to
.scptor.apponly for distribution.
VS Code build task (osacompile)
{
"label": "Compile AppleScript",
"type": "shell",
"command": "osacompile -o build/main.scpt source/main.applescript",
"group": "build"
}Diff strategy
- Prefer text sources for review and diffs.
- If binary scripts are unavoidable, generate compiled artifacts in CI, not source control.
AppleScript → JXA Translation Checklist
Use this after prototyping in AppleScript. This guide covers systematic translation patterns for converting AppleScript automation to JavaScript for Automation (JXA).
Translation Tools & Resources
Modern Python Alternatives (Recommended)
PyXA - Modern Python replacement for JXA:
- Advantages: Active development, better documentation, modern syntax
- Installation:
pip install mac-pyxa(latest: 0.3.0.1) - Requirements: Python 3.10+, PyObjC 9.x
- Documentation: https://skaplanofficial.github.io/PyXA/
- Migration: Often easier to rewrite JXA in PyXA than translate directly
PyObjC - Direct Python-Objective-C bridge:
- Advantages: Access to all macOS frameworks, execute AppleScript/JXA from Python
- Installation:
pip install pyobjc - Documentation: https://pyobjc.readthedocs.io
Legacy JXA Resources
Official Apple Resources
Community Tools
- osascript2js: Command-line tool for basic AppleScript → JXA conversion
- JXA-Cookbook: GitHub repository with translation examples
- Script Debugger: Commercial tool with built-in AppleScript ↔ JXA conversion
Online Converters
- Various GitHub Gist tools for syntax translation
- Community forums (MacScripter, Reddit r/applescript) with conversion examples
Core Syntax Mapping
Application Context
| AppleScript | JXA | Notes |
|---|---|---|
tell application "App" | const app = Application("App"); | Use Application() constructor |
tell app "Finder" | const finder = Application("Finder"); | Standard app reference |
tell current application | const currentApp = Application.currentApplication(); | Self-reference |
Property Access
| AppleScript | JXA | Notes |
|---|---|---|
name of document | document.name() | Property reads are method calls |
set name of doc to "X" | doc.name = "X" | Property writes are assignments |
get properties of doc | doc.properties() | Properties method for all attributes |
exists document "test" | app.documents.byName("test") !== null | Existence checking |
Collections & Indexing
| AppleScript | JXA | Notes |
|---|---|---|
every document | app.documents() | Returns array of objects |
document 1 | app.documents[0] | Zero-based indexing |
first document | app.documents[0] | Same as indexing |
last document | app.documents[app.documents.length - 1] | End of array |
front window | app.windows[0] | Active/front window |
Control Flow
| AppleScript | JXA | Notes |
|---|---|---|
if condition then | if (condition) { | Standard JS syntax |
repeat with i from 1 to 10 | for (let i = 0; i < 10; i++) | Zero-based loops |
repeat while condition | while (condition) | Standard while loop |
try ... on error | try { ... } catch (error) | Exception handling |
Common Patterns
| AppleScript | JXA | Notes |
|---|---|---|
whose filters | filter() or whose() | JXA supports both |
tell System Events | Application("System Events") | UI scripting context |
delay 1 | delay(1) | Delay in seconds |
do shell script "cmd" | app.doShellScript("cmd") | Shell execution |
Translation Verification Steps
Step-by-Step Conversion Process
1. Test AppleScript Original: Run AppleScript in Script Editor to confirm it works 2. Identify Application Context: Map tell application blocks to Application() calls 3. Convert Property Access: Change property of object to object.property() or object.property = value 4. Handle Collections: Convert every item and indexing to array operations 5. Translate Control Flow: Convert AppleScript conditionals and loops to JavaScript equivalents 6. Handle Special Cases: Convert whose filters, UI scripting, and shell commands 7. Test JXA Version: Run converted script and compare outputs 8. Debug Issues: Use console.log() for debugging, check for zero-based indexing errors
Dictionary Verification
- Script Editor Check: File → Open Dictionary → Select target app → Verify command availability
- Property Types: Note whether properties return values or functions in JXA
- Method Signatures: Check if methods require parameters (e.g.,
save({in: path})) - UI Scripting: Confirm accessibility permissions for System Events operations
Whose Clause Translation
// AppleScript
every document whose name contains "report"
// JXA Option 1: Native whose (may be slow)
const docs = app.documents.whose({name: {_contains: "report"}})();
// JXA Option 2: JavaScript filter (often faster)
const docs = app.documents().filter(doc => doc.name().includes("report"));Output Handling
- CLI Integration: Use
JSON.stringify(result)for structured output - Debug Logging:
console.log()goes to stderr; use only for debugging - Error Handling: Wrap in try/catch blocks for robust error reporting
Common Translation Pitfalls & Solutions
Core Language Differences
Function vs Property Access
Pitfall: JXA collections are often functions, not direct properties
// Wrong - treats as property
const docs = app.documents; // Returns function reference
// Correct - call as function
const docs = app.documents(); // Returns arrayMethod Calls for Property Reads
Pitfall: Property reads require method calls, writes are direct assignments
// AppleScript
set name of doc to "New Name"
get name of doc
// JXA
doc.name = "New Name"; // Assignment
const name = doc.name(); // Method callError-Prone Patterns
Zero-Based Indexing
Pitfall: JXA uses zero-based indexing, AppleScript uses one-based
// AppleScript
document 1 // First document
// JXA
app.documents[0] // First document (zero-based)Collection Length Checks
Pitfall: Accessing indexed elements without checking array length
// Unsafe - crashes if no windows
const frontWindow = app.windows[0];
// Safe - check length first
const frontWindow = app.windows.length > 0 ? app.windows[0] : null;UI Scripting Considerations
System Events Requirements
Pitfall: UI scripting requires System Events application and accessibility permissions
// AppleScript
tell application "System Events"
click button 1 of window 1 of process "App"
end tell
// JXA
const systemEvents = Application("System Events");
systemEvents.processes.byName("App").windows[0].buttons[0].click();Permission Setup
- Enable Accessibility permissions in System Settings → Privacy & Security
- Test UI scripting with simple operations first
- Handle permission denied errors gracefully
Performance & Optimization
Whose vs Filter Performance
Pitfall: whose clauses can be slow on large collections
// Slower - native whose
const largeSet = app.documents.whose({modifiedDate: {_greaterThan: date}})();
// Faster - JavaScript filter (for large datasets)
const largeSet = app.documents().filter(doc =>
doc.modifiedDate() > date
);Batch Operations
Pitfall: Individual operations in loops are inefficient
// Inefficient - individual saves
docs.forEach(doc => doc.save());
// Better - batch where possible, or accept individual operations
docs.forEach(doc => {
// Perform modifications
doc.save(); // Individual saves may be necessary
});File System Operations
Path Object Requirements
Pitfall: Some apps require Path() objects, not strings
// Wrong - string path
doc.save({in: "/path/to/file.txt"});
// Correct - Path object
doc.save({in: Path("/path/to/file.txt")});File Existence Checks
Pitfall: JXA file operations don't auto-create directories
// Check directory exists before saving
const fileManager = Application("Finder");
const parentDir = Path("/path/to").toString();
// Note: File existence checking varies by app dictionaryTiming & Synchronization
Delay Units
Pitfall: delay() uses seconds, not milliseconds
// AppleScript
delay 0.5 // Half second
// JXA
delay(0.5); // Half second (same as AppleScript)UI Operation Timing
Pitfall: UI operations need delays for proper execution
systemEvents.processes.byName("App").windows[0].buttons[0].click();
delay(0.5); // Allow UI to respondOutput & Integration
JSON Output for CLI
Pitfall: Mixing stdout/stderr in CLI tools
// Correct - structured JSON output to stdout
console.log(JSON.stringify({result: data, count: data.length}));
// Debug only - stderr
console.error("Debug info:", debugData);Error Handling
Pitfall: Unhandled exceptions break automation
try {
// Automation code
const result = app.doSomething();
console.log(JSON.stringify(result));
} catch (error) {
console.error(`Error: ${error.message}`);
process.exit(1); // Non-zero exit for CLI tools
}Cross-Version Compatibility
Dictionary Changes
Pitfall: App dictionaries change between macOS versions
- Always test on target macOS version
- Check Script Editor dictionary for current method signatures
- Handle missing methods gracefully with try/catch
Deprecation Warnings
Note: JXA and AppleScript are legacy technologies. Consider modern alternatives like Shortcuts for new automation projects.
Complete Translation Examples
Example 1: Basic Document Operations
AppleScript Original:
tell application "TextEdit"
make new document
set text of front document to "Hello World"
save front document in file "output.txt"
end tellJXA Translation:
const textEdit = Application("TextEdit");
// Create new document
textEdit.documents.push(textEdit.Document());
// Set content and save
const doc = textEdit.documents[0];
doc.text = "Hello World";
doc.save({in: Path("output.txt")});Example 2: Filtered Collection Processing
AppleScript Original:
tell application "Finder"
set targetFolder to folder "Documents" of home
set largeFiles to every file of targetFolder whose size > 1000000
repeat with aFile in largeFiles
-- Process large files
log name of aFile
end repeat
end tellJXA Translation:
const finder = Application("Finder");
const homeFolder = Path.home();
const targetFolder = `${homeFolder}/Documents`;
// Get files larger than 1MB
const largeFiles = finder.folders.byName(targetFolder)
.items.whose({_and: [
{kind: "file"},
{size: {_greaterThan: 1000000}}
]})();
// Process each file
largeFiles.forEach(file => {
console.log(file.name());
// Additional processing here
});Example 3: UI Scripting with Error Handling
AppleScript Original:
tell application "System Events"
tell process "Safari"
click button "New Tab" of window 1
delay 0.5
set value of text field 1 of window 1 to "https://example.com"
keystroke return
end tell
end tellJXA Translation:
try {
const systemEvents = Application("System Events");
const safari = systemEvents.processes.byName("Safari");
// Click New Tab button
safari.windows[0].buttons.byName("New Tab").click();
// Wait for UI to update
delay(0.5);
// Enter URL and submit
const addressField = safari.windows[0].textFields[0];
addressField.value = "https://example.com";
// Simulate Return key
systemEvents.keystroke("\r");
} catch (error) {
console.error(`UI automation failed: ${error.message}`);
// Handle accessibility permission issues
}PyXA Translation (Recommended Modern Approach):
import PyXA
try:
safari = PyXA.Safari()
# Open new tab and navigate
safari.open_location("https://example.com")
# PyXA handles UI interactions more reliably than JXA
# No need for explicit UI scripting in many cases
except Exception as e:
print(f"Safari automation failed: {e}")Example 4: Calendar Event Creation
JXA Original:
const Calendar = Application("Calendar");
const event = Calendar.Event({
summary: "Meeting",
startDate: new Date(),
endDate: new Date(Date.now() + 3600000) // 1 hour later
});
Calendar.events.push(event);PyXA Translation:
import PyXA
calendar = PyXA.Calendar()
# Create event
event = calendar.events().push({
"summary": "Meeting",
"start_date": datetime.now(),
"end_date": datetime.now() + timedelta(hours=1)
})Testing & Validation
Unit Testing Approach
1. Isolate Components: Test each translation segment separately 2. Compare Outputs: Ensure JXA and AppleScript produce identical results 3. Handle Edge Cases: Test with empty collections, missing files, permission errors 4. Performance Comparison: Time operations and optimize bottlenecks
Common Testing Scenarios
- Empty collections (
documents()returns[]) - Missing applications (app not installed)
- Permission denied (accessibility/UI scripting)
- File system errors (path not found, permission denied)
- Network timeouts (for remote operations)
Debugging Tools
- Script Editor: Test JXA directly with debugging console
- Console.app: Check system logs for automation errors
- Activity Monitor: Monitor process behavior during automation
- Accessibility Inspector: Debug UI element hierarchies (Xcode tool)
UI scripting inspection tips
Accessibility Inspector
- Use Xcode's Accessibility Inspector to locate element hierarchy.
- Prefer named elements and descriptive attributes over index paths.
Slow operations
entire contentsis expensive; use sparingly.
AppleScript whose and batching patterns
Prefer server-side filters
tell application "Finder"
set oldPNGs to (every file of folder "Documents" of home whose name extension is "png" and modification date < ((current date) - 30 * days))
end tellBatch property reads
tell application "Finder"
set names to name of every file of desktop
end tellAvoid per-item property reads in loops
- Each property access is an Apple Event (slow).
- Push filters and bulk reads into the app when possible.
#!/usr/bin/env python3
"""
Run a read-only AppleScript against common Mac apps to trigger Automation prompts.
This helps grant Terminal/Python permission to control each app before running real automations.
"""
import subprocess
import sys
from textwrap import dedent
APP_SCRIPTS = {
"Calendar": dedent(
"""
tell application "Calendar"
activate
set calendarNames to name of every calendar
return "Calendars: " & (calendarNames as text)
end tell
"""
),
"Notes": dedent(
"""
tell application "Notes"
activate
set accountNames to name of every account
set folderNames to name of every folder
return "Accounts: " & (accountNames as text) & " | Folders: " & (folderNames as text)
end tell
"""
),
"Mail": dedent(
"""
tell application "Mail"
activate
set accountNames to name of every account
set inboxNames to name of every mailbox of inbox
return "Accounts: " & (accountNames as text) & " | Inbox mailboxes: " & (inboxNames as text)
end tell
"""
),
"Keynote": dedent(
"""
tell application "Keynote"
activate
set docNames to name of documents
return "Open documents: " & (docNames as text)
end tell
"""
),
"Microsoft Excel": dedent(
"""
tell application "Microsoft Excel"
activate
set workbookCount to count of workbooks
if workbookCount is 0 then
return "No workbooks open; launch Excel and open/create one to confirm."
else
set workbookNames to name of workbooks
return "Open workbooks: " & (workbookNames as text)
end if
end tell
"""
),
"Reminders": dedent(
"""
tell application "Reminders"
activate
set listNames to name of every list
return "Lists: " & (listNames as text)
end tell
"""
),
}
def run_applescript(app_name: str, script: str) -> tuple[int, str, str]:
"""Run an AppleScript snippet for a specific app via osascript."""
result = subprocess.run(
["osascript", "-e", script],
capture_output=True,
text=True,
)
stdout = result.stdout.strip()
stderr = result.stderr.strip()
if stdout:
print(f"{app_name} responded: {stdout}")
if result.returncode != 0:
print(f"{app_name} failed ({result.returncode}): {stderr or 'no error output'}")
return result.returncode, stdout, stderr
def main() -> int:
print("Triggering Automation prompts for common apps (read-only commands).")
failures = []
for app_name, script in APP_SCRIPTS.items():
print(f"\n=== {app_name} ===")
code, _, _ = run_applescript(app_name, script)
if code != 0:
failures.append(app_name)
if failures:
print("\nSome apps did not respond cleanly. Re-run those after granting permissions:")
for name in failures:
print(f"- {name}")
return 1
print("\nAutomation prompts completed for all apps.")
return 0
if __name__ == "__main__":
sys.exit(main())
#!/usr/bin/env bash
# Trigger Automation prompts for common Mac apps using read-only AppleScript calls.
# Run this once after enabling Terminal/Python in System Settings > Privacy & Security > Automation.
# Run from the same host you intend to automate with (e.g., Terminal vs Python) so the right app is approved.
set -uo pipefail
run_script() {
local app_name="$1"
local script="$2"
echo ""
echo "=== ${app_name} ==="
if ! osascript -e "$script"; then
echo "Warning: ${app_name} script reported an error; check System Settings > Privacy & Security > Automation and retry."
return 1
fi
}
failures=0
run_script "Calendar" 'tell application "Calendar"
activate
set calendarNames to name of every calendar
return "Calendars: " & (calendarNames as text)
end tell'
failures=$(( failures + $? ))
run_script "Notes" 'tell application "Notes"
activate
set accountNames to name of every account
set folderNames to name of every folder
return "Accounts: " & (accountNames as text) & " | Folders: " & (folderNames as text)
end tell'
failures=$(( failures + $? ))
run_script "Mail" 'tell application "Mail"
activate
set accountNames to name of every account
set inboxNames to name of every mailbox of inbox
return "Accounts: " & (accountNames as text) & " | Inbox mailboxes: " & (inboxNames as text)
end tell'
failures=$(( failures + $? ))
run_script "Keynote" 'tell application "Keynote"
activate
set docNames to name of documents
return "Open documents: " & (docNames as text)
end tell'
failures=$(( failures + $? ))
run_script "Microsoft Excel" 'tell application "Microsoft Excel"
activate
set workbookCount to count of workbooks
if workbookCount is 0 then
return "No workbooks open; launch Excel and open/ create one to confirm."
else
set workbookNames to name of workbooks
return "Open workbooks: " & (workbookNames as text)
end if
end tell'
failures=$(( failures + $? ))
run_script "Reminders" 'tell application "Reminders"
activate
set listNames to name of every list
return "Lists: " & (listNames as text)
end tell'
failures=$(( failures + $? ))
echo ""
if [[ "$failures" -eq 0 ]]; then
echo "Automation prompts completed."
else
echo "Automation prompts finished with ${failures} issue(s). Re-run the failing app(s) after granting permissions."
fi