Boilerplate to Add AppleScript to Your macOS App in 2020

There’s not a lot to do, but the documentation is (1) old, (2) not maintained anymore, (3) very wordy. The steps involved are actually very simple. I’ll go through them and provide detail and links for further reading.

Open the Script Editor app. You’ll want to use that for test-driving your app.

Enable AppleScript in Your App

The steps from the now archived docs are:

  1. You need an .sdef file with the script command definitions,
  2. reference this file in your Info.plist, and
  3. enable AppleScript in your Info.plist as well.

First, create a script definition file ending in .sdef. It usually has the same name as your app, but you can pick whatever you want. For TableFlip, this file would be TableFlip.sdef, for The Archive I picked TheArchive.sdef.

Xcode won’t help you with a template here, so you might as well use touch from the Terminal or your favorie text editor and drag the file into your project later.

This is the bare minimum container boilerplate:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE dictionary SYSTEM "file://localhost/System/Library/DTDs/sdef.dtd">
<dictionary title="YOUR APP NAME HERE Terminology">
</dictionary>

Update 2023-03-29: The <dictionary> element used to contain a new namespace, xmlns:xi="http://www.w3.org/2003/XInclude", but unless xi:include or similar are used, this isn’t needed. Thanks to August Mohr for asking what this is for, which prompted me to simplify the code! (The document type definition at /System/Library/DTDs/sdef.dtd already contains the namespace anyway, if I understand it correctly.)

Replace the title with the name of your app plus the word “Terminology”. That appears to be a convention.

Add this file to your Xcode project and make sure it’s part of the app target. You may want to double-check that the target’s Build Phases > Copy Bundle Resources now contains your .sdef file.

Second, open your app target’s Info.plist in Xcode and add two new rows:

  1. In the first row, enable AppleScript support. Set the key to NSAppleScriptEnabled (will become “Scriptable”) and the value to YES (it’s a boolean Xcode will recognize).
  2. In the next row, define which .sdef file exposes available commands. Set the key to OSAScriptingDefinition (will become “Scripting definition file name”) and the value to the file name of your .sdef, e.g. “TableFlip.sdef”. No need to specify a path.

Now build your project in Xcode.

Reveal the build product in Finder and drag the app bundle onto the Script Editor. When you drag app bundles onto the Script Editor, it’ll show a browsable scripting definition, aka the terminology your app exposes.

Since the boilerplate is empty, this should also be empty. For now.

Add default Cocoa commands to e.g. quit your app

How do you get started with defining script commands? – Thankfully, there’s a default set of commands that is provided as a quick start.

You can open it in Xcode straight from the Terminal:

open /System/Library/ScriptingDefinitions/CocoaStandard.sdef

It looks like this at the moment of writing on my machine:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE dictionary SYSTEM "file://localhost/System/Library/DTDs/sdef.dtd">

<dictionary title="Standard Terminology">

  <suite name="Standard Suite" code="????" description="Common classes and commands for all applications.">

    <command name="open" code="aevtodoc" description="Open a document.">
      <direct-parameter description="The file(s) to be opened.">
        <type type="file"/>
        <type type="file" list="yes"/>
      </direct-parameter>
	  <result description="The opened document(s).">
        <type type="document"/>
        <type type="document" list="yes"/>
      </result>
    </command>

    ... lines 19--263 follow ...

  </suite>

</dictionary>

That’s the same structure as the boilerplate from above. You see that inside of a terminology <dictionary> element, there is a <suite> element that contains many <command> elements.

Your .sdef can only have 1 root level <dictionary> element, only one!, but that can contain multiple suited to group different purposes.

If you want, copy the whole code from the opening <suite> until the closing </suite> into your .sdef file. Note that you will want to have a look at each command and change it so it fits your app. Not everything from the CocoaStandard.sdef makes sense.

Let’s look at an example to see how you can customize this.

Customize the standard Quit command

If you look at the definition of the “quit” command in the CocoaStandard.sdef file, you’ll see this (broken onto multiple lines to preserve space):

<command name="quit" code="aevtquit" description="Quit the application.">
    <cocoa class="NSQuitCommand"/>
    <parameter name="saving" code="savo"
            type="save options" optional="yes"
            description="Should changes be saved before quitting?">
        <cocoa key="SaveOptions"/>
    </parameter>
</command>

In my note-taking app The Archive, there’s no notion of manual saving. Files are saved as you type. Yet the command definition above offers an optional parameter to the “quit” command. I don’t need that, so it has to go. I delete the whole <parameter>...</parameter> node.

My .sdef looks like this afterwards:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE dictionary SYSTEM "file://localhost/System/Library/DTDs/sdef.dtd">
<dictionary title="YOUR APP NAME HERE Terminology">
    <suite name="Standard Suite" code="????" description="Common classes and commands for all applications.">
        <command name="quit" code="aevtquit" description="Quit the application.">
            <cocoa class="NSQuitCommand"/>
        </command>
    </suite>
</dictionary>

You can paste this into your .sdef, too. Save the file, build the project, then drag the app bundle onto Script Editor again. It does not auto-update just because the underlying bundle did change during compilation.

Lo and behold – your first command appears!

The quit command is now available!

Test your commands

With Script Editor still open, create a new document. That’ll be the scratchpad for AppleScript interaction.

Use the power of the new “quit” command to see how things work.

tell application "THE APP NAME" to quit

You have to replace THE APP NAME with your app’s name. It appears to work well with the product name. In my case, that’ll be The Archive with a space, or TableFlip without one.

First, run the app from Xcode so it is open.

Then execute the script.

Did your app close? If so, congratulations!

If not, it could be because you typed

# Warning: will not work!
tell "THE APP NAME" to quit

That’ll send the “quit” message to the string. You want to send it to the application with that name.

I cannot show what an app looks like when it is quit. But I can show you the result of the command in Script Editor.

Where to go from here

I think it’s a good first step to redact the CocoaStandard.sdef to fit your app.

Heads up: Removing commands like the “quit” command from your app’s .sdef will actually not disable them. The app still responds to the standard messages; apparently, these are inherited and the suite with the same name and identifier can be overridden. So the commands just don’t show up in the Script Editor terminology view.

When you have cleaned up the “Standard Suite”, you can start adding your own.

To prototype your app’s scriptability, the archived docs recommend you start with the .sdef file first and don’t actually implement any functionality. Instead, you write test scripts in Script Editor and hit the “Compile” button. That’ll verify the syntax and show errors if you mistyped the invokation in the script, or botched the definition in you .sdef file.

I really like the idea of outside-in development of the syntax to see what kind of interactions you will want, and then you figure out how to create the appropriate command implementations.

You will be putting your commands into your own <suite></suite> node, with a unique name and a unique 8-character identifier. Whatever that is for, and why-ever that is limited to 8 characters. Legacy, I guess. We’ll have a look at custom command creation in another post.

Please note that you need to run your app from Xcode before you execute a Script Editor script. If you don’t, macOS’s launch services may figure that the best fit for an application of that name is in you /Application folder. Run your app from Xcode to make this debug version occupy the name and receive the AppleScript events.

Further reading: