Using launchd agents to schedule scripts on macOS
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
.
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>
.
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.