user@mac — osascript — 132×44

osascript(1) the AppleScript & JavaScript-for-Automation field manual

One binary, 175 KB, unchanged in shape since 2001, and still the only supported way to drive a Mac application from a shell. osascript runs OSA scripts — plain text or compiled — in any installed scripting component, and prints whatever the script returns. Everything interesting about it is what sits underneath: the Open Scripting Architecture, Apple events, per-application terminology dictionaries, and since Mojave a TCC permission gate on every event you send. This sheet covers the command-line surface, both shipped languages, the Standard Additions vocabulary, the permission model, and the traps — all verified against the build installed on this machine.

macOS 26.6.2 (25G83) osascript v410 · arm64e+x86_64 AppleScript 2.8 components ascr · jscr · scpt signed 2026-08-01 man page last touched 2014-04-24

01 · The Model

What osascript actually is, and why a one-line shell command can rearrange your Finder windows. Read this once; the rest of the sheet assumes it.

Four layers, bottom up

mental model

Nothing 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: windowcwin, namepnam. 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 forms

OSA 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.

FormWhat it is
.applescript
.scptd/.js
Plain UTF-8 text. Recompiled on every run — slower to start, diffable, and safe in version control.
.scptFlat compiled script, data fork only. Not text; osadecompile reads it back. This is what Script Editor saves by default.
.scptdBundled compiled script — a folder with Contents/Resources/Scripts/main.scpt, so it can carry resources and a nested Script Libraries folder.
.appApplet (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 jscr

Both compile to the same events and reach the same apps. Choose on ergonomics, not capability.

Verdict
AppleScriptDefault. 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.
JavaScriptPick 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.

$ osalang -Lascr appl cgxervdh AppleScript (AppleScript.)jscr appl cgxe-v-h JavaScript (JavaScript)scpt appl cgxervdh Generic Scripting Systemjscr lacks r (recording) and d (dialects) — no other gaps

Where it can and can't run

session context

osascript is a GUI process. Its ability to do anything depends on which session it lands in.

ContextWorks?
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 userNo app scripting. No Aqua session to connect to; app targets fail with -600/-10810.
LaunchAgentYes — agents run inside the GUI session. The correct way to schedule scripting.
LaunchDaemon / cronRuns in the system context: no session, no TCC identity, no dialogs. Use launchctl asuser $UID … to hop into the user's session.
sudo osascriptavoidRuns 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.

02 · Running Scripts

Four ways to feed source in, how arguments arrive, and the shell quoting that makes it survivable.

Synopsis

verified on v410
usage: osascript [-l language] [-e script] [-i]         [-s {ehso}] [programfile] [argument ...]
-e stmtOne line of script. Repeatable; lines are joined in order. If any -e is present, no filename is read — a trailing path becomes an argument.
-l langComponent to compile plain text with. Case-sensitive: AppleScript, JavaScript, Generic Scripting System. js, javascript, jscr all fail with no such component.
-iInteractive REPL. Any -e/file is loaded but not run first, then you get a prompt; each line's result prints as => value in source form. libedit is linked, so arrow keys and history work.
-s {ehso}Output style, in exclusive pairs — last one wins. Concatenate or repeat the flag.
programfilePath to plain text or a compiled script. - means stdin (required if you also want to pass arguments).
argument…Passed as a list of strings to the run handler's direct parameter.

The four input paths

precedence: -e > file > stdin
1 · inline, one line$ osascript -e 'return 6 * 7'42 2 · inline, many lines — repeat -e$ osascript -e 'set x to 5' -e 'return x * x'25 3 · a file (plain text or compiled)$ osascript build.applescript arg1 arg2 4 · stdin — note the bare - when passing args$ echo 'return 6*7' | osascript$ echo 'on run a return item 1 of a end run' | osascript - hello

The heredoc form is almost always the right one for anything over two lines — it sidesteps every quoting problem below:

$ osascript <<'AS'tell application "System Events" return name of first process whose frontmost is trueend tellAS

Language detection

extension-driven

Undocumented in the man page but real on this build: a .js file is compiled as JavaScript automatically. Everything else defaults to AppleScript.

$ cat f.jsJSON.stringify([1,2])$ osascript f.js[1,2]any other extension → AppleScript → syntax error$ osascript f.txtf.txt:0:5: script error: A unknown token can't goafter this identifier. (-2740)-l overrides the extension, in both directions$ osascript -l AppleScript f.js # now it fails

Compiled scripts carry their own component, so a JXA .scpt runs correctly with no -l at all.

Arguments

argv

Everything after the script is handed to the run handler as a list of strings — never numbers, never nested.

AppleScripton run argv if (count argv) < 1 then error "need an arg" number 2 return "hello, " & item 1 of argvend runJavaScriptfunction run(argv) { return argv.join("|") }both$ osascript a.applescript world → hello, world

Leading dashes: arguments after the filename are passed through untouched (--flag arrives intact). With -e, use -- to end option parsing — getopt consumes the -- itself.

$ osascript -l JavaScript \    -e 'function run(a){return JSON.stringify(a)}' \    -- --flag["--flag"]

Numbers arrive as text: coerce with (item 1 of argv) as integer or parseInt(argv[0]).

Shell quoting

the daily tax

AppleScript wants double quotes; the shell wants to eat them. Three reliable strategies, in order of preference.

1 · Single-quote the whole -e

$ osascript -e 'display alert "hi"'

Fails the moment the script needs an apostrophe. The escape is '\'' — close, escaped quote, reopen:

$ osascript -e 'return current application'\''s name'

2 · Heredoc <<'AS'

Quoting the delimiter turns off all shell expansion. No escaping at all. Use this by default.

3 · Interpolate values safely

Never paste shell variables into script text — one " in a filename and you have an injection. Pass them as argv instead:

# wrongosascript -e "tell app \"Finder\" to open \"$f\""# rightosascript - "$f" <<'AS'on run argv tell application "Finder" to open (POSIX file (item 1 of argv))end runAS

Going the other way, quoted form of escapes an AppleScript string for do shell script: it's a "test"'it'\''s a "test"'.

Shebangs

executable scripts

XNU splits shebang arguments on whitespace (Linux does not), so the multi-word form works on macOS:

#!/usr/bin/osascriptreturn "plain AppleScript"verified working on 26.6.2 — both forms#!/usr/bin/osascript -l JavaScript"JXA via shebang"#!/usr/bin/env osascript -l JavaScript

Then chmod +x and run it like any other script. A file named *.js doesn't even need the -l. For anything you'll ship, prefer osacompile -o tool.app so it gets a bundle identifier — TCC grants are keyed to identity, and a bare text file inherits whatever launched it.

Interactive mode

-i
$ osascript -i2 + 2=> 4"hi"=> "hi"preload definitions without running them$ osascript -i -e 'on sq(n) return n*n end sq'sq(7)=> 49

Results always print in source form (quoted, braced), regardless of -s. One statement per line — there's no multi-line continuation — so build helpers with -e up front, or drop into Script Editor for anything structural. Ctrl-D exits.

It also accepts a pipe, which makes it a serviceable batch evaluator: printf '1+1\n2+2\n' | osascript -i.

03 · Results, Errors & Exit Codes

What lands on stdout, what lands on stderr, and how to make either of them parseable. This is where shell integration lives or dies.

-s output style

e · h · o · s
FlagEffect
hHuman-readable (default). Strings unquoted, list braces dropped, records flattened to key:value.
sSource form. Unambiguous and re-compilable — quoted strings, braces, date "…", missing value.
eScript errors to stderr (default).
oScript errors to stdout. For test harnesses that want to match error text without swallowing other diagnostics.
the ambiguity -s h creates$ osascript -e 'return {"foo","bar"}'foo, bar$ osascript -e 'return {a:1, b:{"x", true}}'a:1, b:x, true…and what -s s gives you instead$ osascript -s s -e 'return {a:1, b:{"x", true}}'{a:1, b:{"x", true}}

Flags concatenate (-s so) and repeat (-s h -s s → source wins). If a shell script parses osascript output, use -s s — or, better, return JSON.

Exit codes

only three
CodeMeaning
0Script ran to completion. A script that returns nothing prints nothing and still exits 0.
1Any script error — compile error, runtime error, or an explicit error. Also a missing script file.
2Usage error: unknown option. (osadecompile also uses 2 for OSA failures.)

The AppleScript error number is not the exit code. error number 42 still exits 1. To propagate a status, shell out at the end or parse the message:

$ osascript -e 'error "boom" number 42'; echo $?6:12: execution error: boom (42)1a real exit status, from inside the scriptdo shell script "exit 3" # raises, code in the message

error number -128 ("User canceled") is the conventional quiet abort — Script Editor and applets swallow it silently, but osascript still prints it and exits 1.

Error message anatomy

stderr
6:12: execution error: boom (42)│ │ │ │ └ error number│ │ │ └ message│ │ └ execution | script | compilation└─┴ character offsets into the source

The offsets are into the compiled source text, which for -e means the joined lines — useful for pointing at the failure, useless as line numbers. Errors from a file are prefixed with its path.

a.applescript:0:5: script error: A unknown token  can't go after this identifier. (-2740)

Errors raised by a target app name the app: System Events got an error: osascript is not allowed assistive access. (-25211). That prefix tells you which process refused, which is the first thing to know.

The carriage-return trap

-1 line, endless pain

bites everyonedo shell script converts every \n in the captured output to \r, and osascript — since 10.1 — passes bytes through untouched. Multi-line results come back as one apparent line.

$ osascript -e 'do shell script "printf \"a\nb\n\""' \  | xxd00000000: 610d 620a a.b. ← 0d, not 0afix 1 — ask for it back (2.3+)do shell script "…" without altering line endingsfix 2 — fix it downstream$ osascript … | tr '\r' '\n'

Literal AppleScript strings are unaffected — a return "a\nb" written across two lines emits real newlines. It is specifically the do shell script round-trip that rewrites them.

Returning structured data

the good pattern

Human-readable output is for humans. When a shell script is consuming the result, emit JSON and let jq deal with it.

JXA — free, use it$ osascript -l JavaScript -e '  const se = Application("System Events")  JSON.stringify(se.processes.name())' | jq -r '.[]' AppleScript — hand it to JXA, or build itset json to run script "JSON.stringify(" & ¬  "['a','b'])" in "JavaScript"

Coercion rules for printed results: lists join with ", ", records print key:value, missing value prints literally, dates print in the user's locale format (date "Thursday, August 20, 2026 at 5:44:12 PM" under -s s), aliases as HFS paths (alias Macintosh HD:etc:), and POSIX file as file Macintosh HD:private:etc:hosts. Locale-formatted dates are a parsing hazard — return (d as «class isot») as string or a Unix epoch instead.

04 · The Four Commands

osascript has three siblings and two cousins, all in /usr/bin, all dated 2026-08-12 on this build.

osacompile — text → script object

v410
osacompile [-l lang] [-e cmd] [-o name] [-x] [-d]   [-r type:id] [-t type] [-c creator] [-s] [-u] [file…]
-o nameOutput path. The extension picks the format.app → applet bundle, .scptd → script bundle, anything else → flat file. Default a.scpt.
-l langComponent for plain-text inputs. Same case-sensitive names as osascript.
-e cmdOne line, prepended to any file source. Repeatable.
-xSave execute-only: source is stripped. Irreversible — keep the original.
-sApplet stays open after run (so on idle fires). .app only.
-uApplet shows a startup screen. .app only.
-dScript in the data fork. The default; you will never need to type it.
-r t:idScript in a resource fork resource — classic Mac OS compatibility.
-t / -cFour-character file type / creator code. Unset unless given.
multiple inputs concatenate into one script$ osacompile -o tool.scpt lib.applescript main.applescripta double-clickable app, ad-hoc signed automatically$ osacompile -o Backup.app -s backup.applescript.: replacing existing signature

The generated Info.plist already carries NSAppleEventsUsageDescription and LSRequiresCarbon, and CFBundleName is taken from the -o filename.

osalang — what's installed

-dlL
$ osalang # bare names, for -l$ osalang -d # default → AppleScript$ osalang -l # + subtype, manufacturer, flags$ osalang -L # + description

The eight capability letters, each present or -:

ccompiling scripts
ggetting source data (decompiling)
xcoercing script values
emanipulating event create/send
rrecording scripts
vone-step "convenience" execution
ddialects
hscripts as Apple event handlers

Third-party components (Satimage, JavaScriptOSA forks) would appear here too. On a stock 26.6 install there are exactly three.

osadecompile — script object → text

one argument
$ osadecompile t.scpton run argv return "got " & (count argv) & ": " & (argv as text)end runexecute-only scripts refuse, exit 2$ osadecompile x.scptosadecompile: x.scpt: errOSASourceNotAvailable (-1756).

Round-tripping is not byte-exact: you get the component's canonical pretty-printing (tabs, keyword casing, re-resolved terminology), not your original formatting or comments in their original places. It works on JXA scripts too, where it returns the JavaScript verbatim.

If the app that defined a term is missing, decompiled source shows raw chevrons: «class docu». That's a terminology-resolution failure, not corruption.

sdef / sdp — dictionaries

Xcode-shipped, /usr/bin shims

sdef extracts an application's scripting dictionary as XML — the authoritative answer to "what can I say to this app?"

$ sdef /System/Library/CoreServices/Finder.app$ sdef /Applications/Safari.app | xmllint --format -the built-in vocabulary every script gets$ sdef /System/Library/ScriptingAdditions/\StandardAdditions.osaxevery command name in a dictionary$ sdef /Applications/Mail.app | \  xmllint --xpath '//command/@name' -

sdp converts a .sdef into other forms — -fh for a C header of four-char codes, -fa for Cocoa scripting .plists, -fs for a Scripting Bridge header. Mostly of interest when writing the app, not the script.

For reading rather than grepping, Script Editor's File ▸ Open Dictionary renders the same data with inheritance resolved, and switches between AppleScript and JavaScript syntax with the toggle in the corner — the single best way to learn an unfamiliar app's object model.

05 · AppleScript 2.8

The default language. English-like syntax hiding a prototype-object language with a very unusual scoping model and a genuinely strange approach to strings.

Types

the whole list
TypeNotes
text / stringUnicode since 2.0. "a" & "b" concatenates. No escapes except \", \\, \n, \t, \r.
integer
real
Silently interconvert. 2 ^ 101024.0 (a real). Big values print in E-notation.
booleantrue / false.
list{1, "a", {2}}. 1-indexed. & concatenates.
record{name:"x", n:2}. Keys are terminology, not strings — you cannot enumerate them or build one from a variable key. Access with name of r.
datecurrent date. Subtracting two dates gives seconds. Locale-formatted on output.
alias
file
POSIX file
Three different things. alias must exist and tracks moves; file/POSIX file are references that need not exist.
scriptFirst-class object with properties and handlers. Definable inline with script … end script.
missing valueThe null. Not false, not "".
«class xxxx»Raw four-char type with no local terminology. Legal to write and coerce to.

Syntax

by example
variables · set assigns, copy deep-copiesset x to 5copy {1,2} to y # y is independentset {a, b} to {1, 2} # destructuring conditionalsif x > 3 then set x to 0if x is 0 thenelse if x > 10 thenelseend if loopsrepeat with i from 1 to 10 by 2repeat with f in myList # f is a reference!repeat while x < 5repeat until x = 5repeat 3 timesrepeat # forever; needs exit repeat handlerson square(n) return n * nend squareon greet given name:n, loudly:l # labeled paramssquare(4) # callmy square(4) # call on self, inside a tell errorstry error "nope" number 900on error msg number num return msg & "/" & numend try comments# hash -- dashes (* block *) line continuation — option-L types ¬set x to 1 + ¬ 2

Text is a minefield

where the hours go

There is no split, no join, no replace. There is one global you mutate instead:

joinset AppleScript's text item delimiters to ", "set s to {"a","b","c"} as text # → "a, b, c" splitset AppleScript's text item delimiters to ","set parts to text items of "a,b,c" # → {"a","b","c"} replace = split then joinset AppleScript's text item delimiters to "-"set t to text items of sset AppleScript's text item delimiters to "_"set s to t as text ALWAYS restore it — it is process-globalset old to AppleScript's text item delimitersset AppleScript's text item delimiters to old

Substrings & inspection

characters of "abc"{"a","b","c"}words of swhitespace-split, punctuation droppedparagraphs of sline-split — the sane onetext 2 thru 3 of "abcd""bc"; also text 1 thru -2offset of "b" in "abc"2, or 0 if absents contains "b"substring tests starts with / ends withprefix / suffix

noteComparison is case-insensitive by default"A" is "a" is true. Wrap in considering case … end considering when it matters. considering/ignoring also take diacriticals, hyphens, punctuation, white space.

tell and the terminology stack

the core idea

Inside a tell, the target app's dictionary is pushed onto the name-resolution stack, and unqualified commands are sent to it as Apple events. This is why the same word means different things in different blocks.

tell application "Finder" set n to name of home # Finder's "name" my log_it(n) # my = call my own handlerend tell one-liner formtell application "Finder" to activate nesting — each level adds terminologytell application "System Events" tell process "Finder" tell menu bar 1 … end tellend tell reduce chatter: one event, not fourtell application "Finder" to get ¬ {name, index} of every window

my / of me escapes back to your own script — without it, a call to your own handler is sent to the app, which answers -1708 doesn't understand. This is the single most common beginner error.

tell application id "com.apple.finder" targets by bundle ID, which survives renames and localization. using terms from application "Finder" lets you compile against a dictionary you'll target dynamically at runtime.

References & whose-clauses

the query language

An object specifier is a query, not a value — it's evaluated by the app, so filtering happens on the far side of one event instead of a loop of thousands.

every window whose visible is truefirst document whose modified is truefiles of folder x whose name ends with ".log"item 1 of / last item of / some item ofitems 2 thru -1 ofwindows 1 through 3a reference to (name of window 1) # lazycontents of ref # dereference exists — the safe probeif exists window 1 then …if (count windows) > 0 then …

gotcharepeat with f in someList binds references, not values. if f is "x" can fail where if contents of f is "x" succeeds. Coerce with contents of f, or index the list directly.

Scripts, libraries, pragmas

structure

Script objects

script Counter property n : 0 on bump() set n to n + 1 return n end bumpend scriptCounter's bump()

Libraries — ~/Library/Script Libraries/

Drop a compiled MyLib.scpt (or .scptd) there; all three forms below work on this build. A .scptd/applet bundle can also carry its own Contents/Resources/Script Libraries/.

use MyLib : script "MyLib"MyLib's greet("world")-- or, unaliased:use script "MyLib"script "MyLib"'s greet("world")

Loading at runtime

set s to load script POSIX file "/path/lib.scpt"s's greet("x")store script s in POSIX file "/path/out.scpt"run script "return 2+3"run script "[1,2].length" in "JavaScript"

-1752load script requires a compiled file. Point it at plain text and you get "Script doesn't seem to belong to AppleScript."

Pragmas — must be first

use AppleScript version "2.4"minimum language versionuse scripting additionsrequired if you also use anything elseuse framework "Foundation"AppleScriptObjCproperty parent : AppleScriptinheritance

trapThe moment you write any use statement, Standard Additions stops being implicit — add use scripting additions or display dialog mysteriously stops resolving.

AppleScriptObjC

the escape hatch

Since 10.10, plain AppleScript can call Cocoa directly — which is how you get real string handling, JSON, HTTP, and file APIs.

use framework "Foundation"use scripting additionsset p to current application's NSProcessInfo's processInfo()return p's processName() as textset u to current application's NSUUID's UUID()'s UUIDString()return u as text

Rules: Objective-C method names lose their colons and gain underscores — stringByReplacingOccurrencesOfString:withString: becomes stringByReplacingOccurrencesOfString_withString_(a, b). Every class lives under current application's. Cocoa objects are not AppleScript values: coerce on the way out (as text, as list, as integer) or you'll be comparing opaque references.

Frameworks beyond Foundation need their own use framework "AppKit", "CoreImage", etc. This all runs fine under osascript — no Xcode, no app bundle required.

06 · JavaScript for Automation

Component jscr, shipped since Yosemite and never documented again. A JavaScriptCore engine with an Apple-event bridge bolted to the global object — plus the full Objective-C runtime.

The one rule

specifiers vs values

Every property access builds an object specifier — a lazy query. Calling it sends the event and returns a real JavaScript value. Forget the parentheses and you'll be inspecting a query object.

const se = Application("System Events")se.processes[0].name # a specifier, not a string→ Application("System Events").processes.at(0).namese.processes[0].name() # "Finder" plural properties vectorize — one event, N resultsse.processes.name() # ["Finder","Chrome",…]se.processes.length # 70 — length is the exception

Setting is plain assignment: app.windows[0].bounds = [0,0,800,600]. Commands are methods on the app or on a specifier: app.activate(), doc.close({saving:"no"}).

Getting a handle on an app

Application()
Application("Finder") by nameApplication("com.apple.finder") by bundle idApplication.currentApplication() osascript itselfApplication("Safari").running() doesn't launch itApplication("Safari").activate() doesApplication("Mail").launch() launch without activating standard additions must be opted intoconst app = Application.currentApplication()app.includeStandardAdditions = trueapp.displayDialog("hi")app.doShellScript("echo hi")app.pathTo("home folder").toString()

Names are camelCased from the dictionary: display dialogdisplayDialog, do shell scriptdoShellScript, background onlybackgroundOnly. Parameters become one options object: displayDialog("q", {defaultAnswer:"", withTitle:"T"}).

delay(n), Path(p), Progress, Ref(), Automation.getDisplayString(v) and ObjC are global — no includeStandardAdditions needed.

whose() filters

app-side queries
se.processes.whose({backgroundOnly: false}).name()se.processes.whose({name: {_contains: "in"}}).name() operators_equals _contains _beginsWith _endsWith_greaterThan _lessThan_and: [ {…}, {…} ] _or: [ {…}, {…} ]_not: {…} combinedapp.windows.whose({_and: [  {visible: true},  {name: {_beginsWith: "Untitled"}}]})

Same semantics as AppleScript's whose: the filter is evaluated inside the target app, so it stays one round-trip no matter how many objects it scans. A JavaScript .filter() over .name() results is the slow way to do the same thing.

The ObjC bridge

$ and ObjC
ObjC.import("Foundation") Foundation is preloadedObjC.import("AppKit")$.NSHomeDirectory().js → "/Users/you" (unwrap)ObjC.unwrap(nsval) same, explicitObjC.deepUnwrap(nsDictOrArray)recursive → JS object$(jsValue) wrap JS → NS colons become underscores$.NSString.stringWithString("hi")$.NSDictionary.dictionaryWithObjectForKey("v","k")$.NSFileManager.defaultManager  .contentsOfDirectoryAtPathError("/tmp", null) out-parameters need a Refconst err = Ref()$.NSString.stringWithContentsOfFileEncodingError(  p, $.NSUTF8StringEncoding, err)

This is the whole macOS SDK, callable from a one-liner: NSWorkspace, NSPasteboard, NSUserNotification, NSTask, CoreImage. ObjC.registerSubclass and ObjC.bindFunction exist for delegates and C functions when you really need them.

Bridged values are NS objects. $.NSString… is not a JS string until you .js it — typeof and === will lie to you otherwise.

Output & errors

JXA-specific

The value of the last expression is the script's result — there is no implicit return at top level, so wrap object literals in parentheses: ({a:1}), not {a:1}.

$ osascript -l JavaScript -e '({a:1})'a:1$ osascript -s s -l JavaScript -e '({a:1})'{"a":1}console.log goes to stderr, the result to stdout$ osascript -l JavaScript -e 'console.log("x"); 1' 2>/dev/null1

That split is a feature: console.log for progress, the return value for data. Errors are real JS exceptions carrying the OSA number:

try { app.windows[0].name() }catch (e) { e.errorNumber # -1728           e.message # "Can't get object." }

JSON in, JSON out is the reason to reach for JXA at all: JSON.stringify(Application("System Events").processes.name()) is a complete, pipe-safe inventory in one line.

JXA gotchas

the short list
  • Missing parens — the number-one bug. A specifier stringifies to source, so a wrong value often looks plausible in output.
  • No await that matters. Events are synchronous; there is no event loop to run promises in. delay(n) is your only timer.
  • Reserved-ish names. A dictionary property called length, name on a collection, or anything colliding with Object.prototype resolves unpredictably; use .at(i) and explicit calls.
  • Errors from apps are opaque"Can't get object." with no clue which object. Bisect by evaluating sub-specifiers.
  • Library("Name") loads from ~/Library/Script Libraries the same as AppleScript's use script, and reaches AppleScript libraries fine.
  • No sourcemaps, no debugger in osascript. Script Editor's JavaScript mode plus console.log is the whole toolkit.

07 · Standard Additions

The vocabulary every script gets for free, from /System/Library/ScriptingAdditions/StandardAdditions.osax (v410). Nine suites, 55 commands — the complete list below, with the real parameter names from this build's dictionary. Optional parameters are marked ?.

User Interaction

14 commands
display dialogtext; default answer?, hidden answer?, buttons {…}?, default button?, cancel button?, with title?, with icon (stop|note|caution|file)?, giving up after?dialog reply record: button returned, text returned, gave up
display alerttext; message?, as (informational|warning|critical)?, buttons?, default/cancel button?, giving up after? → alert reply
display notificationtext; with title?, subtitle?, sound name? — fire-and-forget; needs Notification permission for the calling app
choose filewith prompt?, of type {"public.text"}?, default location alias?, invisibles?, multiple selections allowed?, showing package contents? → alias
choose file namewith prompt?, default name?, default location? → file (need not exist)
choose folderwith prompt?, default location?, invisibles?, multiple selections allowed?, showing package contents? → alias
choose from listlist; with title?, with prompt?, default items?, OK/cancel button name?, multiple selections allowed?, empty selection allowed? → list, or false on cancel
choose applicationwith title?, with prompt?, multiple selections allowed?, as type class?
choose remote applicationBonjour app picker; needs remote Apple events
choose URLshowing {Web servers|FTP Servers|Telnet hosts|File servers|News servers|Directory services|Media servers|Remote applications}?, editable URL? → the chosen URL. The Bonjour service browser; the card's fourteenth command.
choose colordefault color {r,g,b}? → RGB list, 16-bit channels
saytext; using "Samantha"?, speaking rate?, pitch?, modulation?, volume?, waiting until completion?, saving to file?, displaying?, stopping current speech?
beepinteger? — how many times
delaynumber — seconds, fractions allowed

-128Cancelling any chooser or dialog raises User canceled. Always wrap interactive commands in try, or use cancel button.

Files & Paths

File Commands + Read/Write
path toA folder constant or an application; from (system|local|network|user|Classic) domain?, as type class?, folder creation? → alias
path to resourceNamed resource inside a script bundle or app
info forfile; size? → record: name, displayed name, name extension, file type, type identifier, size, creation date, modification date, folder, package folder, visible, busy status, default application, short version, long version
list folderalias; invisibles? → list of names
list disks→ list of mounted volume names
mount volumeurl; on server?, in AppleTalk zone?, as user name?, with password?
open for accessfile; write permission? → file reference number
close accessThe reference number or file — always in a try/cleanup
readfile/refnum; from?, for?, to?, before?, until?, using delimiter(s)?, as type class?
writedata to file; starting at?, for?, as type class?
get eof / set eofFile length in bytes

path to constants

application support · applications folder · desktop · desktop pictures folder · documents folder · downloads folder · favorites folder · Folder Action scripts · fonts · help · home folder · internet plugins · keychain folder · library folder · movies folder · music folder · pictures folder · preferences · printer descriptions · public folder · scripting additions folder · scripts folder · services folder · shared documents · shared libraries · sites folder · startup disk · startup items · system folder · system preferences · temporary items · trash · users folder · utilities folder · workflows folder · voices (+ a dozen Classic-era leftovers)

POSIX path of (path to downloads folder)path to me # the running scriptpath to application "Safari"path to frontmost application

Read a whole UTF-8 file: read POSIX file p as «class utf8». Reading as text assumes MacRoman and mangles anything non-ASCII.

Miscellaneous

the workhorses
do shell scripttext; as type class?, administrator privileges?, user name?, password?, with prompt?, altering line endings? → stdout as text
current date→ date
time to GMT→ offset in seconds
system info→ record: AppleScript version, system version, short/long user name, user ID, user locale, home directory, boot volume, computer name, host name, IPv4 address, primary Ethernet address, CPU type, physical memory
system attributeEnv var by name, or a four-char gestalt selector: system attribute "sysv"
get volume settings→ record: output volume, input volume, alert volume, output muted (0–100)
set volumeoutput volume 0–100?, input volume?, alert volume?, output muted?
random numberfrom?, to?, with seed? — integer bounds give an integer
roundnumber; rounding (up|down|toward zero|to nearest|as taught in school)?
open locationURL text; error reporting? — hands it to the default handler

String & clipboard

offsetoffset of "b" in "abc" → 2, or 0
ASCII character / numberDeprecated but alive; byte-oriented
localized stringFrom a bundle's .strings; from table?, in bundle?
summarizeThe 1990s text summarizer, still shipping
the clipboardas type class?the clipboard as text
set the clipboard toAny value; coerced to the pasteboard
clipboard info→ list of {type, byte count} pairs

Scripting & folder actions

load / store scriptCompiled scripts only (-1752 otherwise)
run scriptSource text or file; in "JavaScript"?, with parameters {…}?
scripting components→ list of installed OSA language names
Folder ActionsHandlers, not commands: on adding folder items to, removing folder items from, opening folder, closing/moving folder window for

passwordwith administrator privileges prompts for a password unless you pass user name/password — which puts a plaintext credential in your script. Prefer a sudoers rule or a keychain lookup via security find-generic-password -w.

08 · Talking to Applications

System Events is the universal donor: it scripts the things that have no script interface of their own — processes, the UI, plists, XML, power, login items.

System Events suites

the map
SuiteWhat it reaches
ProcessesEvery running process, and — with Accessibility — its entire UI tree. Commands: click, keystroke, key code, select, perform.
Disk-Folder-FileDisks, folders, files, aliases, packages. move, delete, open.
Property ListRead/write plists as objects — property list file, property list item, value.
XMLXML file, XML element, XML attribute.
Powersleep, restart, shut down, log out.
Login ItemsList/add/remove startup items.
Network / Screen Saver / Dock / Appearance / Security / CD&DVDPreference objects, mostly superseded by defaults and networksetup but still readable.
Desktop, AccountsWallpaper picture, current user record.
Scripting DefinitionIntrospect any app's dictionary from a script.

UI scripting

last resort, but it works

When an app has no dictionary, drive its Accessibility tree. Requires Accessibility permission for the calling process — separate from Automation.

tell application "System Events" tell process "Safari" set frontmost to true click menu item "New Window" of ¬ menu 1 of menu bar item "File" of ¬ menu bar 1 keystroke "l" using command down key code 36 # return end tellend tell

Finding the element

entire contents of window 1 # dump the treeproperties of UI element 1 of … # role, title, valuename of every UI element of window 1

Xcode's Accessibility Inspector (or the old UI Browser) beats guessing. Expect brittleness: element indexes shift between releases, and localization changes every visible title — prefer matching on description or role over position.

Modifier keywords for keystroke: command down, option down, control down, shift down, combinable as a list.

Finding what an app can do

discovery loop
sdef /Applications/Foo.appthe raw dictionaryls /Applications/Foo.app/Contents/Resources/*.sdefoften there verbatimdefaults read …/Info.plist NSAppleScriptEnabledis it scriptable at all?osascript -e 'tell app "Foo" to properties'everything at the top levelosascript -e 'tell app "Foo" to ¬
  name of every window'
probe an element classosalang / Script Editor ▸ Open Dictionaryrendered, with inheritance

An app with no .sdef and no NSAppleScriptEnabled is not scriptable — UI scripting or nothing. Electron and most cross-platform apps land here.

Useful built-in targets beyond System Events: Finder (files, windows, selection), Terminal/iTerm, Safari (do JavaScript in a tab), Mail, Music, Notes, Reminders, Calendar, Messages, Photos, Keynote/Pages/Numbers (unusually complete dictionaries), Script Editor, and Image Events (headless image manipulation).

09 · Permissions

Since Mojave, every Apple event crosses a consent boundary. Most "it works in Script Editor but not from my script" reports end here.

How TCC sees your script

the identity problem

Consent is recorded per (client, target) pair, and the client is the process that owns the session — not osascript. A script run from Terminal is authorized as Terminal; from an editor's integrated shell, as that editor; from a LaunchAgent, as the agent's program. Move the same script to a different terminal and you get prompted again.

PermissionNeeded for
AutomationSending any Apple event to another app. Prompted once per (client, target) pair — authorizing System Events says nothing about Safari.
AccessibilityUI scripting: click, keystroke, key code, and reading any UI element. Never auto-prompts usefully — grant it by hand.
Full Disk AccessReading protected locations (Mail, Messages, Safari data) even via do shell script.
Screen RecordingWindow titles of other apps, and any screenshot path.
Notificationsdisplay notification — attributed to the client app.

A denied prompt is sticky. The dialog never returns; you have to clear it manually.

Granting, revoking, diagnosing

System Settings ▸ Privacy & Security
the panes, by namePrivacy & Security ▸ Automation # per client→targetPrivacy & Security ▸ Accessibility # UI scriptingPrivacy & Security ▸ Full Disk Access reset a decision so it prompts again$ tccutil reset AppleEvents$ tccutil reset AppleEvents com.apple.Terminal$ tccutil reset Accessibility com.apple.Terminal$ tccutil reset All com.example.myapplet watch the decisions being made$ log stream --predicate \  'subsystem == "com.apple.TCC"' --info what am I, as far as TCC is concerned?$ osascript -e 'return name of ¬    current application' # → osascript

The Automation pane only lists pairs that have already been asked about — you cannot pre-authorize a target from the UI. To force the prompt, run the smallest possible script against it.

tipShip recurring automation as an osacompile -o Foo.app applet. It has a stable bundle identifier and its own TCC row, so grants survive terminal changes and it can carry a real NSAppleEventsUsageDescription string for the prompt.

What refusal looks like

real messages, this build
Accessibility not grantedSystem Events got an error: osascript is notallowed assistive access. (-25211) Automation denied or not yet askedNot authorized to send Apple events to Finder.(-1743) no GUI session to talk toApplication isn't running. (-600)Can't get application "Foo". (-1728)

-1743 means declined; a first-time request instead blocks on a modal prompt, so a script running unattended will appear to hang. Wrap first contact in with timeout of 10 seconds when scheduling.

10 · Error Codes

The numbers in parentheses at the end of every error line. Negative codes are Apple's; positive ones are yours to define.

Script & runtime

OSA
#Meaning
-128User canceled. The conventional quiet abort.
-1700Can't make x into type y — coercion failure.
-1701Required parameter missing.
-1704Parameter of the wrong type.
-1708Object doesn't understand the message. Usually a missing my.
-1712Apple event timed out (default 120 s). Raise with with timeout of N seconds.
-1719Invalid index / can't get reference.
-1728Can't get x — object doesn't exist, or the app isn't there.
-1743Not authorized to send Apple events. TCC.
-1752Script doesn't belong to AppleScript — load script on plain text.
-1753Script error, unspecified.
-1756errOSASourceNotAvailable — execute-only script.
-2700Generic error "…" with no number.
-2701Divide by zero.
-2740Compile error: unknown token after identifier.
-2741Compile error: expected x but found y.
-2752Run handler specified twice, or top-level statements and an on run.
-2753Variable is not defined.
-2763Uncaught error number n.

Process & system

Carbon / AE
#Meaning
-600Application isn't running (procNotFound). No GUI session, or the app quit.
-609Connection is invalid — the target died mid-conversation.
-610No user interaction allowed. Something tried to put up a dialog in a non-GUI context.
-1Generic Carbon failure. Unhelpful by design.
-43File not found.
-48Duplicate file name.
-50Parameter error.
-108Out of memory.
-1400 / -5000Volume / access denied.
-10004Privilege violation — the app refused the operation.
-10006Can't set x to y — read-only property.
-10010Can't handle this command in the current state.
-10660Application isn't openable (Gatekeeper / damaged bundle).
-10810Unknown launch error. Classically: no session, or LaunchServices is confused.
-25211Not allowed assistive access. Accessibility permission.

Your own error "msg" number N should use a positive number, or the reserved user range starting at 500, to stay out of Apple's space.

Handling them well

patterns
catch, classify, re-raisetry tell application "Finder" to get name of window 1on error msg number n if n is -1728 then return "no window" else if n is -1743 then error "grant Automation for Finder" number 500 else error msg number n # preserve the original end ifend try the fuller signatureon error msg number n from offender partial result r ¬    to expectedType guaranteed cleanup — no finally, so do bothset fh to open for access f with write permissiontry write "x" to fh close access fhon error try close access fh end tryend try bump the 120-second event timeoutwith timeout of 600 seconds tell application "Photos" to export …end timeout

11 · Cookbook

One-liners that earn their keep. Anything marked tcc needs an Automation grant for the named app the first time it runs.

System & session

no permissions needed
everything about this machine, as a recordosascript -e 'return system info' frontmost apposascript -e 'tell application "System Events" to ¬  return name of first application process ¬  whose frontmost is true' visible apps, as JSONosascript -l JavaScript -e 'JSON.stringify(  Application("System Events").applicationProcesses  .whose({backgroundOnly:false}).name())' login itemsosascript -e 'tell application "System Events" to ¬  return name of login items' volume: read, set, muteosascript -e 'return output volume of (get volume settings)'osascript -e 'set volume output volume 25'osascript -e 'set volume with output muted' sleep the display / the machineosascript -e 'tell application "System Events" to sleep' bundle path for an app, instantly (see Traps)osascript -l JavaScript -e 'ObjC.import("AppKit");  $.NSWorkspace.sharedWorkspace  .URLForApplicationWithBundleIdentifier(    "com.apple.Safari").path.js'

Dialogs, alerts, notifications

shell → GUI
ask for a line of text; exits 1 on Cancelname=$(osascript -e 'text returned of (display dialog ¬  "Your name?" default answer "" ¬  with title "Setup")') || exit 1 yes/noosascript -e 'button returned of (display dialog "Ship it?" ¬  buttons {"No","Yes"} default button "Yes")' password fieldosascript -e 'text returned of (display dialog "Passphrase:" ¬  default answer "" with hidden answer)' pick from a listosascript -e 'choose from list {"dev","stage","prod"} ¬  with prompt "Target?"' file picker → POSIX pathosascript -e 'POSIX path of (choose file ¬  with prompt "Pick a log" of type {"public.text"})' banner when a long build finishesmake; osascript -e 'display notification "build done" ¬  with title "make" sound name "Glass"' timed alert that dismisses itselfosascript -e 'display alert "Deploying…" ¬  giving up after 3'

Every chooser raises -128 on Cancel, so || exit 1 in the shell is the natural pairing.

Clipboard & text

verified
read / write (pbcopy's scriptable cousin)osascript -e 'return the clipboard as text'osascript -e 'set the clipboard to "hello"' what flavors are on the pasteboard?osascript -e 'return clipboard info'# Unicode text, 32, «class utf8», 16, … copy a file as a file, not as its pathosascript -e 'set the clipboard to ¬  (read (POSIX file "/tmp/a.png") as «class PNGf»)' strip formatting from the clipboardosascript -e 'set the clipboard to ¬  ((the clipboard as text) as text)' speak stdinosascript -e 'on run a  say item 1 of a using "Samantha"end run' "deployment complete"

Files

System Events + Standard Additions
read a UTF-8 file properlyosascript -e 'return read (POSIX file "/etc/hosts") ¬  as «class utf8»' write one (truncating)osascript -e 'set f to open for access ¬  (POSIX file "/tmp/out.txt") with write permission' ¬  -e 'set eof f to 0' -e 'write "hi" to f' ¬  -e 'close access f' metadata recordosascript -e 'return info for (POSIX file "/etc/hosts")' a plist value, without plutilosascript -e 'tell application "System Events" to ¬  return value of property list file ¬  "/System/Library/CoreServices/SystemVersion.plist"' move to Trash (recoverable, unlike rm)osascript -e 'tell application "Finder" to ¬  delete POSIX file "/tmp/junk"' # tcc: Finder standard foldersosascript -e 'POSIX path of (path to downloads folder)'

Apps

tcc — one grant per target
Finder: current folder, selectionosascript -e 'tell application "Finder" to ¬  return POSIX path of (target of front window as alias)'osascript -e 'tell application "Finder" to ¬  return selection as alias list' Safari: front tab URL, and run JS in itosascript -e 'tell application "Safari" to ¬  return URL of front document'osascript -e 'tell application "Safari" to ¬  do JavaScript "document.title" in front document' Chrome: every tab in every windowosascript -l JavaScript -e 'JSON.stringify(  Application("Google Chrome").windows().map(    w => w.tabs().map(t => t.url())))' Music: now playingosascript -e 'tell application "Music" to ¬  return name of current track & " — " & artist ¬  of current track' quit an app politely (it can refuse / prompt to save)osascript -e 'tell application "Preview" to quit' is it running? (does not launch it)osascript -e 'return running of application "Safari"'

Never test with tell app "X" to activate if you only want to know whether X exists — activate launches it. Use running of application "X", which stays quiet.

Shell integration

glue
shell function that always quotes correctlyask() { osascript - "$1" <<'AS'on run argv return text returned of (display dialog ¬ (item 1 of argv) default answer "")end runAS} multi-line output, made line-safeosascript -e '…' | tr '\r' '\n' run a script under the user's GUI session from a daemonlaunchctl asuser 501 /usr/bin/osascript /path/x.scpt a scheduled applet (correct: an Agent, not a Daemon)~/Library/LaunchAgents/com.you.job.plist  ProgramArguments = (/usr/bin/osascript, /path/x.scpt)  StartCalendarInterval = { Hour = 9 } time a script's Apple-event chatterlog stream --predicate 'process == "osascript"' --info

12 · Traps & Field Notes

Everything on this page that surprised someone, collected. All behaviours confirmed on macOS 26.6.2 / osascript v410.

Compiled scripts remember

state on disk

A property in a compiled script is written back to the file at the end of every run. Plain text starts fresh each time. The same three lines behave completely differently depending on which form you saved:

$ osacompile -o cnt.scpt -e 'property n : 0' \    -e 'set n to n + 1' -e 'return n'$ osascript cnt.scpt; osascript cnt.scpt; osascript cnt.scpt123the same source as plain text$ osascript cnt.applescript; osascript cnt.applescript11

Great for a persistent counter or "last run" timestamp. Terrible when a .scpt is on a read-only volume (silent failure) or under version control (the file changes on every run). If you want statelessness, ship text, or reset the property explicitly in on run.

path to application launches the app

changed in 26.6

fixedThe indefinite hang is gone on 26.6.2. On 26.5.x, path to application "Safari" from a terminal-hosted osascript blocked forever — not 120 seconds, not an error. It now returns, but it is still not free: it takes as long as launching the app, because it does launch the app.

timings measured on 26.6.2, apps not already runningpath to application "Stickies" # 4.5 s — and Stickies is now openpath to application "Font Book" # 1.7 spath to application "Podcasts" # 2.8 sthese were always instant, and still arepath to application "System Events"path to frontmost application

So the advice is unchanged, for a different reason: don't use it to test for an app, and don't use it in anything latency-sensitive. The Cocoa call answers from LaunchServices without starting anything.

instant, no launch, no consent promptosascript -l JavaScript -e 'ObjC.import("AppKit");  $.NSWorkspace.sharedWorkspace  .URLForApplicationWithBundleIdentifier("com.apple.Safari")  .path.js'or, from the shell$ open -Ra Safari # exit 0 if it exists, launches nothing

The application id form behaves identically. Note that Safari resolves through the cryptex: alias Preboot:Cryptexes:App:System:Applications:Safari.app:, not Macintosh HD:Applications:.

Silent-wrong-answer traps

no error, bad result
  • -e beats a filename. osascript -e '…' script.scpt runs the -e and passes the path as argv. No warning.
  • -s h flattens structure. {"a","b"} and {{"a",{"b"}}} both print a, b. Parse -s s or JSON, never the default.
  • String comparison ignores case by default — and diacriticals if you ask. considering case when it matters.
  • repeat with x in list binds references. x is "a" can be false where contents of x is "a" is true.
  • Missing parentheses in JXA. A specifier stringifies to plausible-looking source instead of throwing.
  • Dates print in the user's locale — a script that parses its own date output breaks on a machine set to another region. Use as «class isot» or epoch seconds.
  • argv is always strings, even when it obviously isn't. "10" as number first.
  • Record keys are terminology. You cannot iterate a record's keys or index one with a variable. If you need a real map, use JXA or NSDictionary.

Hard-error traps

at least it tells you
  • Language names are case-sensitive. -l javascriptno such component. It's JavaScript.
  • Calling your own handler inside tell sends it to the app: -1708. Prefix with my.
  • Any use statement disables implicit Standard Additions. Add use scripting additions or lose display dialog.
  • on run plus top-level statements is illegal: -2752. Pick one.
  • load script needs a compiled file, not source: -1752.
  • osacompile -x is one-way. osadecompile answers -1756 forever. Keep the source.
  • Cancelling any dialog raises -128, which exits 1 and prints to stderr. Wrap in try.
  • The 120-second Apple-event timeout is not the script timeout. with timeout of N seconds around anything slow.

Environment traps

context matters
  • TCC identifies your host, not osascript. Grants follow the terminal, editor, or LaunchAgent that spawned it — move the script and you're a new client.
  • sudo osascript is almost always wrong. Root has its own (empty) TCC record and usually no session. Use do shell script … with administrator privileges inside the script.
  • LaunchDaemons can't script apps. Use a LaunchAgent, or launchctl asuser $UID.
  • do shell script runs a bare sh — no login shell, no .zshrc, minimal PATH. Use absolute paths.
  • It also converts \n to \r unless you add without altering line endings.
  • Standard Additions is not sandboxed-app-safe; a sandboxed host may see commands vanish from the dictionary.
  • Apple event ordering is not guaranteed across separate tell blocks to a busy app. If a step depends on the last one landing, poll with exists rather than adding delays.

Performance

it's the events

Every property access inside a tell is an IPC round-trip — roughly a millisecond, occasionally far worse if the target is busy drawing. The optimisation is always fewer, fatter events.

N eventsrepeat with w in windows set end of names to name of wend repeatone eventset names to name of every window one event, filtered app-sideevery file of f whose name ends with ".log" and when the work is not app work at alldo shell script "find … -name '*.log'"

Compiled .scpt skips the compile step at launch — worth a few tens of milliseconds on a script that runs in a loop, irrelevant otherwise. If a script feels slow, it's almost never the language.

provenanceEvery command signature, error message, exit code and behavioural claim on this page was executed against macOS 26.6.2 (25G83), osascript v410 (arm64e, signed 2026-08-01), AppleScript 2.8, with StandardAdditions.osax v410. Dictionary listings were re-extracted with sdef from this machine's copies of StandardAdditions.osax and System Events.app. Where behaviour differs from the man page — extension-based language detection, multi-word shebangs — the machine won. Re-verified after the 26.5.2 → 26.6.2 update: osascript, AppleScript and Standard Additions are unchanged at v410 / 2.8; the one behavioural change is path to application, which no longer hangs.