Four layers, bottom up
mental modelNothing here is a normal API call. Every interaction is a message posted to another process, and each layer exists to make that bearable.
1 · Apple events
A 1980s IPC format: a four-character event class and ID (core/getd), an addressed target process, a keyed parameter dictionary, and an optional reply. Structured, typed, asynchronous under the hood, and the only supported channel into another app's object model.
2 · The terminology dictionary (.sdef)
Each scriptable app ships an XML dictionary mapping English-ish words to those four-character codes: window → cwin, name → pnam. This is why the same script reads differently against Finder and Mail — the vocabulary belongs to the app, not the language. Dump one with sdef /Applications/Foo.app.
3 · The OSA scripting component
A pluggable engine that compiles source into a script object and executes it, translating your statements into events. Three are installed (osalang): AppleScript, JavaScript, and a "Generic" dispatcher. Components live in /System/Library/Components/*.component.
4 · osascript
A thin CLI over OSAKit: pick a component, feed it source or a compiled script, hand it argv, run the run handler, print the result. It links AppKit and Carbon and runs as a full GUI-capable process — which is exactly why it can put up a dialog, and why it needs a login session to do anything interesting.
Compiled vs. plain text
two on-disk formsOSA source is compiled to a script object — a serialized token tree plus its resolved terminology and its variable state. That object is what a .scpt file holds.
| Form | What it is |
|---|---|
| .applescript .scptd/.js | Plain UTF-8 text. Recompiled on every run — slower to start, diffable, and safe in version control. |
| .scpt | Flat compiled script, data fork only. Not text; osadecompile reads it back. This is what Script Editor saves by default. |
| .scptd | Bundled compiled script — a folder with Contents/Resources/Scripts/main.scpt, so it can carry resources and a nested Script Libraries folder. |
| .app | Applet (or droplet, if it has an on open handler): a bundle whose MacOS/applet stub runs the embedded script. osacompile ad-hoc code-signs it for you. |
gotchaA compiled script's property and top-level global values are saved back into the file when it finishes. Compiled scripts are mutable state on disk; plain text is not. See Traps.
Which language?
ascr vs jscrBoth compile to the same events and reach the same apps. Choose on ergonomics, not capability.
| Verdict | |
|---|---|
| AppleScript | Default. Every example on the internet, every app's dictionary is written in its idiom, and the error messages are legible. Its string/list handling is miserable; its tell blocks are unbeatable. |
| JavaScript | Pick when the logic is real work — JSON, arrays, string munging, math. Real closures, real objects, JSON.parse. Documentation is thin (the 2014 release notes and nothing since), and the property-vs-method rule bites constantly. |
A pragmatic split: JXA when the script mostly computes, AppleScript when it mostly commands. You can bridge — run script "…" in "JavaScript" works in either direction, and both languages reach Objective-C.
Where it can and can't run
session contextosascript is a GUI process. Its ability to do anything depends on which session it lands in.
| Context | Works? |
|---|---|
| Terminal / SSH as the logged-in user | Yes, if that user has an active Aqua session. Events reach apps on the console. Dialogs appear on the user's screen. |
| SSH as another user | No app scripting. No Aqua session to connect to; app targets fail with -600/-10810. |
| LaunchAgent | Yes — agents run inside the GUI session. The correct way to schedule scripting. |
| LaunchDaemon / cron | Runs in the system context: no session, no TCC identity, no dialogs. Use launchctl asuser $UID … to hop into the user's session. |
sudo osascript | avoidRuns as root in root's context. Loses the user's TCC grants and often can't find the session. If you need admin, use do shell script … with administrator privileges inside the script instead. |