Using launchd agents to schedule scripts on macOS

12 minute read

Even though launchd has been around for quite some time now, I was still using crontab for scheduling some of my scripts until recently. Since launchd LaunchAgents can do much more and don’t expect your computer to be running at all times, it’s time to start using them more 😎.

Let’s see how we can easily set up a LaunchAgent to run a Python script for the current user in regular intervals:

Write a plist for your agent

Unlike crontab jobs, LaunchAgents are written in (quite verbose) plist XML. We define a Label according to the name of our agent and then go on describing how it should behave (which program to run, what arguments, when to run it, etc.).

Here’s is an example with the filename de.davidhamann.my-program.plist which we will place in ~/Library/LaunchAgents.

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>Label</key>
    <string>de.davidhamann.my-program</string>
    <key>ProgramArguments</key>
    <array>
        <string>/usr/local/bin/python3</string>
        <string>/Users/user/path/to/my-program.py</string>
    </array>
    <key>StartInterval</key>
    <integer>30</integer>
</dict>
</plist>

Label

The first interesting element to look at is <key>Label</key>. It precedes a string element which provides the unique name for our agent. This name is generally the same as our plist filename without the extension (both should follow reverse-domain style). The label is required and we need it to later start our agent.

ProgramArguments

Next, we define what program to run and what arguments to pass to it. In the example we will run Python3 from /usr/local/bin and give it the path of our script to execute.

StartInterval

StartInterval defines in what interval we want the program to run and takes an integer expected to contain the number of seconds. In our case, we run my-program.py every 30 seconds.

Load and start the agent 🚀

LaunchAgents can be loaded, started and unloaded with launchctl. To do so, we need to supply the ID of our target user and the plist file we just created. You can get your user-id via id -u or id -u <the-username>, respectively.

Assuming we are in ~/Library/LaunchAgents/ we can now load our agent by executing the following command:

launchctl bootstrap gui/<your-user-id> de.davidhamann.my-program.plist

And then kick-start it with (or just wait 😊):

launchctl kickstart -k gui/<your-user-id>/de.davidhamann.my-program

Note that we use the Label in this command, not the plist filename. The -k options means that the service will first be killed, if running (see man launchctl).

Our agent should now be active and starting the program every 30 seconds.

Note: we are setting up an agent for a user in a gui session. You may want to specify other targets, e.g. system for system-wide services. See man launchctl for more.

Unloading

Unloading an agent is as easy as:

launchctl bootout gui/<your-user-id> de.davidhamann.my-program.plist

In other tutorials you may see the use of launchctl load, launchctl start and launchctl unload. While these commands still work, they are from a from a previous implementation of launchd and are classified as “Legacy”.

Troubleshooting

In case you’re having trouble with an agent, you can either look into system.log or direct the output to a desired location by adding two keys for output and errors to the <dict>.

<key>StandardOutPath</key>
<string>/log/path/out.log</string>
<key>StandardErrorPath</key>
<string>/log/path/error.log</string>

Make sure to bootout, bootstrap, kickstart again after you make your change.

Note: The Debug key, though mentioned in the launchd manual, is not supported anymore.

Other use cases

Another common use case is StartCalendarInterval for starting a program at a given time. A nice property of this is, that it will handle sleep times. So launchd would start the job the next time your computer wakes up and won’t just skip it like cron (it does not do that for StartInterval, though I didn’t find that to be a problem for my cases).

Have a look at man launchd.plist for many more options or visit developer.apple.com for a broader (but in parts outdated) overview.

Like to comment? Feel free to send me an email or reach out on Twitter.

Did this or another article help you? If you like and can afford it, you can buy me a coffee (3 EUR) ☕️ to support me in writing more posts. In case you would like to contribute more or I helped you directly via email or coding/troubleshooting session, you can opt to give a higher amount through the following links or adjust the quantity: 50 EUR, 100 EUR, 500 EUR. All links redirect to Stripe.