Overview

There is a macOS feature called the Open Scripting Architecture (OSA) which provides an infrastructure for intercommunication and automation of macOS software. One of the officially supported OSA languages is JavaScript and its use in this context is called “JavaScript for Automation” which is often abbreviated to “JXA”.

The idea is that instead of using AppleScript, you can program & automate macOS using a more modern language like JavaScript. This gives the programmer access to the enormous ecosystem of javascript libraries and tools in addition to all the modern optimizations and language features of ES6-ish JavaScript. JXA also provides access to a built-in Objective-C bridge that enables access to the file system and the ability to call into the Cocoa frameworks and plain C functions. JXA can basically do anything native code can. It’s a bit like node without needing to install node.

I’m really just learning about JXA now, a full 5 and a half years after it was initially released as part of macOS 10.10 “Yosemite”. My initial excitement has quickly been tempered by some complications:

All that said, JXA support is still there in macOS as of this writing… in all it’s beautiful/powerful/terrible majesty.

Resources

Notes

Many of these come from the resources above, so really do check out the JXA Cookbook and the resources it links to.

Access the REPL

Access the REPL from the terminal with:

osascript -il JavaScript

Shebang Support

JXA scripts can run using the shebang mechanism. Begin your scripts with this line to be able to invoke them directly:

#!/usr/bin/env osascript -l JavaScript

For example:

$ cat neato.js 
#!/usr/bin/env osascript -l JavaScript

console.log("this is cool")
$ chmod +x neato.js 
$ ./neato.js 
this is cool

If you put your code in a function called run, it will be invoked automatically when you run the script and it will be passed the argument vector.

#!/usr/bin/env osascript -l JavaScript

function run(argv) {
  console.log("this is cool", argv.join("-"))
}

running it with some arguments produces:

$ ./neato.js is it not?
this is cool is-it-not?

Object Specifiers

Mostly the things that get thrown around in JXA are called Object Specifiers. They represent data that the Apple Events layer interacts with and they look like this:

Application("Mail").accounts.byId("A8ECDFBC-BF8F-437D-BB52-525E0D1EEDA0").mailboxes.byName("testbox")

From the Apple docs:

Many objects in the JavaScript for Automation host environment refer to external entities, such as other applications, or windows or data inside of those other applications. When you access a JavaScript property of an application object, or of an element of an application object, a new object specifier is returned that refers to the specific property of that object.

…it continues:

You access properties of scripting objects as JavaScript properties using dot notation. As described above, the returned object is an object specifier — a reference to the accessed property—rather than the actual value. To send the get event to the external entity and return its value, call the property as a function:

subject = Mail.inbox.messages[0].subject()

Similarly, setting a property sends the set event to the external entity with the data you want to set.

Mail.outgoingMessages[0].subject = 'Hello world'

Here is an example showing how to get a property from every element of an array (in this case, the subject of every message in Mail’s inbox).

subjects = Mail.inbox.messages.subject()

You can access an Object Specifier’s text representation with Automation.getDisplayString().

Here’s a quick example:

>> var firstMailbox = Application("Mail").accounts[0].mailboxes[0]()

>> console.log(firstMailbox)
!! Error on line 1: Error: Can't convert types.

>> console.log(Automation.getDisplayString(firstMailbox))
Application("Mail").accounts.byId("978F1418-FE48-4A59-A08F-4EA23D387DDC").mailboxes.byName("testbox")

Poking around

  • Try accessing the .properties of an object.
  • Try just expressing the object in the REPL to see what comes back.
  • Observe the apple events that get fired as your script executes using Script Editor.app

Global Scope Notes

This is mostly stuff from the REPL.

Automation

Automation = { 
  "$": "function $",
  "Application": "function anonymous",
  "Library": "function anonymous",
  "ObjC": {"registerSubclass": "function anonymous",

  "ObjectSpecifier": "object ObjectSpecifierConstructor",
  "Path": "function anonymous",
  "Progress": "object Progress",
  "Ref": "function anonymous",

  "bindFunction": "function anonymous",
  "block": "function anonymous",
  "castObjectToRef": "function anonymous",
  "castRefToObject": "function anonymous",
  "deepUnwrap": "function anonymous"},
  "delay": "function anonymous",
  "dict": "function anonymous",
  "getDisplayString": "function anonymous",
  "import": "function anonymous",
  "initializeGlobalObject": "function anonymous",
  "interactWithUser": false,
  "log": "function anonymous",
  "super": "function anonymous",
  "unwrap": "function anonymous",
  "wrap": "function $",
}

Application

Class methods:

Application.currentApplication

Object methods (callable on instantiated Application objects):

Application = {
  "activate": "function anonymous",
  "commandsOfClass": "function anonymous",
  "displayNameForCommand": "function anonymous",
  "displayNameForElementOfClass": "function anonymous",
  "displayNameForPropertyOfClass": "function anonymous",
  "elementsOfClass": "function anonymous",
  "frontmost": "function anonymous",
  "id": "function anonymous",
  "includeStandardAdditions": false,
  "launch": "function anonymous",
  "name": "function anonymous",
  "parentOfClass": "function anonymous",
  "propertiesOfClass": "function anonymous",
  "quit": "function anonymous",
  "running": "function anonymous",
  "strictCommandScope": false,
  "strictParameterType": false,
  "strictPropertyScope": false,
  "version": "function anonymous",
}

Library

Prototype:

{
  "displayNameForPropertyInClass": "function anonymous",
  "enumeratorsForEnumeration": "function anonymous",
  "parameterNamesForCommand": "function anonymous",
  "parameterTypeForNameInCommand": "function anonymous",
  "propertyTypeForNameInClass": "function anonymous",
}

Path

Just the constructor and the toString() method.

Progress

Just an object

ObjectSpecifier

Just an object

delay

Just a function

console.log

Just a function

ObjC

All class methods:

ObjC = {
  "$": "function $",
  "bindFunction": "function anonymous",
  "block": "function anonymous",
  "castObjectToRef": "function anonymous",
  "castRefToObject": "function anonymous",
  "deepUnwrap": "function anonymous",
  "dict": "function anonymous",
  "import": "function anonymous",
  "interactWithUser": false,
  "Ref": "function anonymous",
  "registerSubclass": "function anonymous",
  "super": "function anonymous",
  "unwrap": "function anonymous",
  "wrap": "function $",
}

Ref

A class method: Ref.equals()

And object methods:

Ref = {
  "getProperty": "function anonymous",
  "setProperty": "function anonymous",
  "type": undefined,
}

$

Just a function

Array Filtering with .whose({...})

From the apple docs:

Example that “find[s] all of the messages in your inbox that contain the word JavaScript or the word Automation in the subject”:

Mail.inbox.messages.whose({
  _or: [
    {subject: {_contains: "JavaScript"}},
    {subject: {_contains: "Automation"}}
  ]
})

Supported Filter Operators

operator example
_equals
'='
literal matching
{ name: { _equals: 'JavaScript for Automation' } }
{ name: { '=': 'JavaScript for Automation' } }
{ name: 'JavaScript for Automation' }
_contains { name: { _contains: 'script for Auto' } }
_beginsWith { name: { _beginsWith: 'JavaScript' } }
_endsWith { name: { _endsWith: 'Automation' } }
_greaterThan
'>'
{ size: { _greaterThan: 20 } }
{ size: { '>': 20 } }
_greaterThanEquals
'>='
{ size: { _greaterThanEquals: 20 } }
{ size: { '>=': 20 } }
_lessThan
'<'
{ size: { _lessThan: 20 } }
{ size: { '<': 20 } }
_lessThanEquals
'<='
{ size: { _lessThanEquals: 20 } }
{ size: { '<=': 20 } }
_and
_or
(both require 2 args)
{ _and: [ {name: 'Apple'}, {size: {'<': 20}} ] }
{ _or: [ {name: 'Apple'}, {name: {_contains: 'JavaScript'}} ] }
_not { _not: [ {name: 'Apple'}, {name: {_contains: 'JavaScript'}} ] }

The _and, _or, and _not keys require arrays as their value, additionally the _and and _or arrays requires at least two elements in that array.