Schedule with launchd
Write a minimal LaunchAgent
Create a valid property list with a unique label, explicit program arguments, and one deliberate trigger.
A launchd job is described by a property list, an XML file that says what to run and when. Here is a complete, minimal LaunchAgent for our screenshot sorter:
<?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>com.flaviocopes.screenshot-sorter</string>
<key>ProgramArguments</key>
<array>
<string>/Users/flavio/bin/sort-screenshots</string>
</array>
<key>StartInterval</key>
<integer>900</integer>
</dict>
</plist>
Save it as ~/Library/LaunchAgents/com.flaviocopes.screenshot-sorter.plist. Match the filename to the label. Six months from now, the filename is how you’ll find the job.
Three keys, three decisions.
Label
Label is the job’s identity in your user’s launchd domain. Use a reverse-domain name, unique on the machine. Every launchctl command you run later refers to it.
ProgramArguments
ProgramArguments lists the executable and its arguments, one <string> per element. Use an absolute path for the executable. launchd resolves nothing for you, and there is no PATH to lean on.
Don’t put shell operators in there. launchd starts the executable directly, not through a shell. A > or && arrives as a literal argument to your program, not as redirection. If the job needs shell features, put them inside the script.
StartInterval
StartInterval is the trigger: run every 900 seconds. Other triggers exist. StartCalendarInterval for clock times. WatchPaths for reacting to file changes. RunAtLoad for login.
Keep the first job small. Add one trigger, prove when and why it runs, then add more conditions if you need them. A job with three triggers and no logs is a mystery.
Validate it
Check the file with plutil -lint:
plutil -lint ~/Library/LaunchAgents/com.flaviocopes.screenshot-sorter.plist
# .../com.flaviocopes.screenshot-sorter.plist: OK
A malformed plist is the most common first failure. launchd’s own complaint about it is generic and unhelpful. plutil points at the exact line. I run it after every edit, before anything else.
Nothing runs yet. Writing the file registers nothing with launchd. Loading it comes in two lessons, after we pin down the job’s environment. Skipping that step is how you get a job that runs fine from Terminal and dies on the schedule.
Try this: write the plist for your own task, with your own label and script path, and run plutil -lint on it. Fix anything it reports before moving on.
Lesson completed