Nathan Grigg

Schedule jobs using launchd

(updated

Launchd is Apple’s replacement in OS X for several Unix process management utilities, most notably cron. I use it to run several scripts at scheduled times or fixed intervals. Every day my computer is set to download my twitter statuses and check my library card for overdue books. Every morning my computer resets its volume to medium. Every week it backs up the WordPress database of my “family pictures” blog. It syncs my work files between my computer and the university file server.

Almost anything you can do with cron you can do with launchd, but with more power and flexibility. Unlike cron, launchd does not assume that your computer is always running. So if your computer happens to be sleeping at the time a job is scheduled, it will run the job when it wakes up. This is probably the best feature of launchd, because it allows me to run scripts on my iMac while still letting it sleep when I’m not using it.

I have pieced together what I know about using launchd to schedule jobs from tutorials across the internet, trial and error, and the manuals. This is my attempt to gather all my knowledge about this in one place. If there is something you think I should add or fix, let me know.

(This article has been updated from the original version, most notably with information about new software tools.)

Contents

Because this article is longer than usual.

  1. Quick start
  2. Launchd basics
  3. Tools to manage your agents
    1. Launchctl
    2. LaunchControl
    3. Lingon
    4. Lunchy
  4. Format of the plist file
  5. Permissions
  6. Disabled agents
  7. Random thoughts

Quick start

The best way to get started is to buy LaunchControl.

LaunchControl doesn’t have to stay running in the background. It just sets up the job and gets out of the way, letting OS X do the rest. And since OS X already uses launchd to run just about everything, from Spotlight to ssh-agent to the bezel notifications that appear when you change the volume, scheduling jobs will not add any overhead.

Launchd basics

Each launchd agent is stored in an xml plist file. The file contains information about what program to run, when to run it, which arguments to use, and other options. Although technically you can make things work no matter where the plist file is saved, it is best to put it in ~/Library/LaunchAgents, because plists in this folder are automatically loaded into launchd when you log in.

Each agent has a label, which must be unique. Apple uses reverse domain syntax com.apple.whatever, but it doesn’t matter. The plist filename can be anything, but you would be crazy to use anything other than the label, e.g. com.apple.whatever.plist. Sometimes agents are referred to by the file, and sometimes by the label.

Warning: At some point you will use one of your plists as a template to create another. You will of course give them different filenames, but you will forget to change the label, which means only one of them will work. Hopefully this warning will then enter your mind and you will solve the problem.

Apple uses the terms load and unload to mean that an agent is in the system, ready to go, and start or stop to talk about running or killing the actual process. So all agents in your LaunchAgents folder are loaded when the computer starts up. Then it pays attention to when they are scheduled and starts them at the appropriate time. If you create a new plist file you need to load it manually. If you change a plist file, you need to unload it and load it again.

Tools to manage your agents

Launchctl

Apple provides launchctl to manage your agents. The main commands you need are

launchctl load ~/Library/LaunchAgents/net.nathangrigg.archive-tweets.plist
launchctl unload ~/Library/LaunchAgents/net.nathangrigg.archive-tweets.plist
launchctl start net.nathangrigg.archive-tweets
launchctl stop net.nathangrigg.archive-tweets
launchctl list

Notice that load and unload require the filename, while start and stop require the label. The start command will manually run the job, even if it isn’t the right time. This can be useful for testing. The stop command just kills the process, but is convenient because you don’t need to know the pid. The list command shows all loaded agents, with the pid if they are currently running and the exit code returned the last time they ran.

LaunchControl

I mentioned LaunchControl earlier, and it is the tool I recommend. It can create and edit your plist files, giving you helpful information when you need it. It can start, stop, load, and unload your agents and help you debug them when they fail.

(LaunchControl was released after the first version of this article, and it is good enough that it makes a lot of what is written here unnecessary.)

Lingon

Lingon is a simple tool for creating and managing launchd agents. It does not give you as much control as LaunchControl, but it has been around longer. When you use Lingon to edit an agent, pressing the Save button automatically loads and unloads the job.

Lunchy

One other useful tool is Lunchy by Mike Perham. This is a ruby script that speeds up the loading and unloading of agents by allowing you to refer to a plist by any unique substring of the filename. My only issue is that it uses terminology that conflicts with Apple’s. Lunchy uses start to mean load and stop to mean unload. It mostly compensates by providing very useful commands restart to unload and then load and edit to edit the file in your default editor.

Install it using gem install lunchy and then edit the file using

lunchy edit archive-tweets

Now reload it using

lunchy restart archive-tweets

Format of the plist file

Here is a very basic plist file to run a script every 86,400 seconds.

xml:
 1: <?xml version="1.0" encoding="UTF-8"?>
 2: <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
 3: <plist version="1.0">
 4: <dict>
 5:     <key>Label</key>
 6:     <string>net.nathangrigg.archive-tweets</string>
 7:     <key>ProgramArguments</key>
 8:     <array>
 9:         <string>/Users/grigg/bin/archive-tweets.py</string>
10:     </array>
11:     <key>StartInterval</key>
12:     <integer>86400</integer>
13: </dict>
14: </plist>

There are several other keys you can insert between lines 4 and 13 to activate other options. If you want to see what is available, read the launchd.plist manual.

Note that I have provided the full path to the script, since globs aren’t expanded by default. If you want to expand globs, you can include an EnableGlobbing key followed by <true/>.

If your script requires arguments, you would supply these in extra string tags after line 9.

If you download a script from the internet or write one yourself, make sure it is executable, or this might not work.

By default, anything written to standard out or standard error ends up in the system log. If you would like it to be somewhere else, you can use the StandardOutPath and StandardErrorPath keys.

The KeepAlive key allows a script to be run multiple times, depending on certain conditions. If you set it to <true/>, then the script will be run over and over again forever. The following snippet will rerun the script if it returns a nonzero exit code. Read the xml as “Keep the process alive as long as it doesn’t successfully exit”.

xml:
<key>KeepAlive</key>
<dict>
    <key>SuccessfulExit</key>
    <false/>
</dict>

In most cases, the system will wait 10 seconds between runs of the script to save system resources. You can adjust this with a ThrottleInterval key, which takes an integer argument, and should be outside the KeepAlive dictionary. You can also set the agent to stay alive depending on the (non)existence of an internet connection using NetworkState or of a file using PathState.

In older versions of OS X, there was an OnDemand key which was required. It is now obsolete and has been replaced by KeepAlive, which is optional. Many of the other examples on the internet still have an OnDemand key, but you don’t need it.

Permissions

For security reasons, launchd will not run LaunchAgents whose plist files have the wrong permissions. For example, they must not be writable by anyone other than the owner. Root LaunchAgents stored in /Library/LaunchAgents must be owned by the root user.

Disabled agents

Both launchctl and lunchy allow you to disable an agent using the -w flag with unload/stop. I do not recommend this. An agent that has been disabled will not load when you log in and cannot be loaded using load/start without using the -w flag again. You will probably just be confused later about why an agent is not loading even though it is in the right place. Information about which agents have been disabled in this manner is stored in a separate file. In Lion, this is the file

/var/db/launchd.db/com.apple.launchd.peruser.NNN/overrides.plist

where NNN is your user id number (find it using id -u).

Random thoughts

Your launchd agents are loaded when you log in, but not unloaded when you log out. So the only time your agents aren’t loaded is during the time between a restart and when you log in. If you have multiple users and need something to run no matter who is logged in, you should put it in /Library/LaunchAgents or /Library/LaunchDaemons.

If your computer sleeps often, it will be asleep when jobs should run, which means it will run them right when it wakes up, possibly before it connects to the internet. I have experimented with KeepAlive and NetworkState to get a job to repeat itself until there is a network connection. You could also use SuccessfulExit and write the script so that it only returns a nonzero code to mean “run again in 10 seconds.” Either method would have the script running (and presumably failing) every 10 seconds when you have no internet connection. A better idea would be to just sleep 5 seconds at the beginning of your script. Or you could double the run frequency and hope for the best.