AppleScript & JXA Automation
Overview
Use JavaScript for Automation (JXA) via osascript -l JavaScript for macOS automation. JXA is preferred over AppleScript for its familiar syntax and better data handling.
Getting App Information
Get scripting dictionary
sdef /Applications/AppName.app
sdef /System/Applications/Music.app
Get app properties
osascript -l JavaScript -e 'Application("AppName").properties()'
Core Patterns
Basic app interaction
osascript -l JavaScript -e '
const app = Application("AppName")
app.includeStandardAdditions = true
// ... operations
'
Multi-line scripts
osascript -l JavaScript -e '
const finder = Application("Finder")
const selection = finder.selection()
selection.map(f => f.name())
'
Common Operations
File Operations
// Read file
const app = Application.currentApplication()
app.includeStandardAdditions = true
app.read(Path("/path/to/file"))
// Write file
app.write("content", {to: Path("/path/to/file")})
// Open file for access
app.openForAccess(Path("/path"))
Finder
osascript -l JavaScript -e '
const finder = Application("Finder")
// Get selected files
const selection = finder.selection()
selection.map(f => ({name: f.name(), path: f.url()}))
// Create folder
finder.make({new: "folder", at: finder.desktop, withProperties: {name: "New Folder"}})
// Move file
finder.move(finder.files["test.txt"], {to: finder.folders["Documents"]})
'
Music App
osascript -l JavaScript -e '
const music = Application("Music")
// Search library
const lib = music.playlists.byName("Library")
const results = lib.search({for: "love"})
results.map(t => ({
name: t.name(),
artist: t.artist(),
album: t.album(),
duration: t.duration()
}))
// Playback control
music.play()
music.pause()
music.nextTrack()
music.previousTrack()
// Current track
const track = music.currentTrack
({name: track.name(), artist: track.artist()})
'
Music App - Get Currently Opened Screen/View
Use System Events to inspect the current UI state and visible content:
osascript -l JavaScript -e '
const se = Application("System Events");
const music = se.processes["Music"];
// Get main content title (artist/album/playlist name being viewed)
const mainContent = music.windows[0].splitterGroups[0].scrollAreas[1].lists[0].lists[0].uiElements[0].staticTexts[0];
mainContent.value();
'
Music App - List Items from a UI Section
osascript -l JavaScript -e '
const se = Application("System Events");
const music = se.processes["Music"];
// Get Singles & EPs list (list index 4 in main content)
const singlesEPsList = music.windows[0].splitterGroups[0].scrollAreas[1].lists[0].lists[4];
const groups = singlesEPsList.groups();
const albums = [];
for (const group of groups.slice(1)) {
try {
const btns = group.buttons();
if (btns.length > 0) {
const name = btns[0].name();
if (name && name !== "1") {
albums.push(name);
}
}
} catch (e) {}
}
albums.join("\n");
'
Music App - Get Entire Window Contents (for exploration)
osascript -l JavaScript -e '
const se = Application("System Events");
const music = se.processes["Music"];
music.windows[0].entireContents();
'
Play all tracks by artist
osascript -l JavaScript -e '
const music = Application("Music")
// Search for artist
const lib = music.playlists.byName("Library")
const results = lib.search({for: "Lab'\''s Cloud"})
// Get unique artists from results
const artists = [...new Set(results.map(t => t.artist()))]
// Find the exact artist match
const targetArtist = artists.find(a => a === "Lab'\''s Cloud")
if (targetArtist) {
// Filter tracks by the selected artist
const artistTracks = results.filter(t => t.artist() === targetArtist)
if (artistTracks.length > 0) {
// Play first track and queue the rest
music.play(artistTracks[0])
// Queue remaining tracks
for (let i = 1; i < artistTracks.length; i++) {
music.playPlaylist(lib, false)
}
"Now playing " + targetArtist + " - queued " + artistTracks.length + " tracks"
}
} else {
"Artist not found"
}
'
Notes
osascript -l JavaScript -e '
const notes = Application("Notes")
// Create note
notes.make({new: "note", withProperties: {body: "Hello World"}})
// List folders
notes.folders().map(f => f.name())
// Create note in specific folder
const folder = notes.folders.byName("Notes")
notes.make({new: "note", at: folder, withProperties: {name: "Title", body: "Content"}})
'
Safari
osascript -l JavaScript -e '
const safari = Application("Safari")
// Get current URL
safari.windows[0].currentTab.url()
// Get all tab URLs
safari.windows[0].tabs().map(t => ({name: t.name(), url: t.url()}))
// Open URL
safari.openLocation("https://example.com")
// Find existing tab or open new one
const Safari = Application("Safari")
Safari.activate()
let foundHackerNews = false
const frontmostWindow = Safari.windows[0]
// Iterate over all tabs in the frontmost window
for (let i = 0; i < frontmostWindow.tabs.length; i++) {
const tab = frontmostWindow.tabs[i]
const tabUrl = tab.url()
if (tabUrl && tabUrl.includes("news.ycombinator.com")) {
frontmostWindow.currentTab = tab
foundHackerNews = true
break
}
}
// If Hacker News was not found, open it in a new tab
if (!foundHackerNews) {
const newTab = Safari.Tab({url: "https://news.ycombinator.com"})
frontmostWindow.tabs.push(newTab)
frontmostWindow.currentTab = newTab
}
// Execute JavaScript in page
safari.doJavaScript("document.title", {in: safari.windows[0].currentTab})
'
System Events (UI Automation)
osascript -l JavaScript -e '
const se = Application("System Events")
// List running apps
se.processes().map(p => p.name())
// Click menu item
const proc = se.processes.byName("Finder")
proc.menuBars[0].menuBarItems.byName("File").menus[0].menuItems.byName("New Finder Window").click()
// Keystroke
se.keystroke("v", {using: "command down"})
se.keyCode(36) // Return key
'
Notifications
osascript -l JavaScript -e '
const app = Application.currentApplication()
app.includeStandardAdditions = true
app.displayNotification("Body text", {
withTitle: "Title",
subtitle: "Subtitle",
soundName: "Frog"
})
'
Dialogs
osascript -l JavaScript -e '
const app = Application.currentApplication()
app.includeStandardAdditions = true
// Alert
app.displayAlert("Title", {message: "Details", buttons: ["OK", "Cancel"]})
// Choose file
app.chooseFile({withPrompt: "Select a file"})
// Choose folder
app.chooseFolder({withPrompt: "Select folder"})
// Text input
app.displayDialog("Enter name:", {defaultAnswer: ""})
'
Clipboard
osascript -l JavaScript -e '
const app = Application.currentApplication()
app.includeStandardAdditions = true
// Get clipboard
app.theClipboard()
// Set clipboard
app.setTheClipboardTo("New content")
'
Calendar
osascript -l JavaScript -e '
const cal = Application("Calendar")
// List calendars
cal.calendars().map(c => c.name())
// Create event
const calendar = cal.calendars.byName("Home")
cal.Event({
summary: "Meeting",
startDate: new Date("2024-01-15T10:00:00"),
endDate: new Date("2024-01-15T11:00:00")
}).make({at: calendar})
'
Reminders
osascript -l JavaScript -e '
const rem = Application("Reminders")
// List reminder lists
rem.lists().map(l => l.name())
// Create reminder
const list = rem.lists.byName("Reminders")
rem.Reminder({name: "Buy groceries", dueDate: new Date()}).make({at: list})
'
Tips
- Always check sdef first - Run
sdef /path/to/app.appto see available commands - Use properties() - Quick way to see what's accessible on any object
- Enable standard additions -
app.includeStandardAdditions = truefor dialogs, file ops - Handle arrays - JXA returns special arrays; use
.map()to extract values - Debugging - Add
console.log()and run withosascript -l JavaScript script.js
Error Handling
osascript -l JavaScript -e '
try {
const music = Application("Music")
music.currentTrack.name()
} catch(e) {
"No track playing"
}
'
