OFFICIAL SECURITY BLOG

We’ve moved! You can now read the latest and greatest on Mac adware and malware at Malwarebytes.

Scheduling recurring tasks

Published August 1st, 2012 at 10:11 PM EDT , modified August 1st, 2012 at 10:11 PM EDT

Every now and then I get questions about how to schedule some kind of task that will happen periodically. For example, you may want to copy a file to or from a remote server at a particular time every day, load a particular web site periodically or gather some information and send it as a text to your cell phone every morning at 8 am. The possibilities are endless. There are a number of possible solutions to this problem, which have their advantages and disadvantages. In this article, we’ll explore several of them, but the main focus will be on the “geekiest” of those solutions: LaunchAgents!

Use iCal or Calendar

Creating a Calendar Alarm in Automator

So, let’s take it for granted that you have an AppleScript or Automator app, or something similar, that does what you want automatically and is only waiting for some way to trigger it at specific times. The easiest method for most people is to use an alert in iCal (in Mac OS X 10.7 or earlier) that runs an AppleScript.  Assign that alert to a repeating event of some kind and you’re done… your script will run every time that alert triggers. In Mountain Lion (OS X 10.8), things have changed a little. You have to create a Calendar Alarm in Automator. Once you have saved it, it will be available as a choice when assigning an alert to an event, and that Automator action will run every time that alert triggers.

The main problem with this, in my opinion, is that it clutters up your calendar with things that may not be really calendar-related. If they are related to some existing event in your calendar, then great, problem solved! However, if that’s not ideal for your situation, there are better solutions.

Download an App

Another possibility is to find and download an app designed to run constantly in the background and run tasks on a particular schedule. Such apps may work perfectly fine, and are likely to be easy to use, but I still don’t like the idea. An app tends to be fairly heavy, with interface elements and the like that need to be loaded into memory. In addition, a poorly-coded app running constantly in the background could cause performance issues and other problems. That’s just not something that I want to get involved with.

The final – and my favorite – possibility is the LaunchAgent. A LaunchAgent is something that can be run under specified conditions automatically by the system itself. The problem with LaunchAgents is that creating them is not a task that is particularly easy or transparent for the inexperienced.  Fortunately, I plan to remedy that here with some clear, simple directions.

Set up a LaunchAgent

This is not for the faint of heart. I will try to keep things as simple as possible, but it will be best if you have at least passing familiarity with Unix commands and other technical details of Mac OS X. You will also need some kind of simple text editor. (Pages or Microsoft Word do not count!) TextEdit, found in the Applications folder, can do the job, but I highly recommend downloading TextWrangler, which is a much more powerful text editor. TextWrangler will make some things much easier. You will also need to use the Terminal, which is found in the Utilities folder in the Applications folder, to execute some Unix commands. If you have never used the Terminal before, I have a word of caution for you: be extremely careful with the Terminal! Carelessness with the Terminal can have very bad results. The instructions below include copying commands and pasting them into the Terminal. If you are not Unix-savvy, please be sure to do exactly that, rather than re-typing commands, as a misplaced space or other character can have drastic consequences.

There are two basic parts for setting up a LaunchAgent. The first is the “agent” itself, which is the script or application that gets run periodically. This could be nothing more than an ordinary application, including an application created with Automator, or it could be something a little more complicated. The second is the property list file that defines when the agent runs.

The property list

A property list (or plist) file has a special structure (called XML) that allows you to easily define data structures that the computer can use. In this case, those data structures will tell the computer what file to run (ie, what the agent is), when to run it and what the task should be called. Here’s a very simple example:

<?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>net.reedcorner.test</string>
   <key>ProgramArguments</key>
   <array>
      <string>/Applications/Contacts.app/Contents/MacOS/Contacts</string>
   </array>
   <key>StartInterval</key>
   <integer>300</integer>
</dict>
</plist>

Copy this text and put it into a file named net.reedcorner.test.plist.  You can save it on your desktop for now.

This may look a little intimidating, but it’s really not too tough to understand. If you don’t understand XML format, you just have to remember some basic patterns. First, for example, is the <dict> tag that starts the part of the file you’ll be interested in changing.  Following that, there is a key-string pair.  The key identifies the object that follows, in this case telling you that it’s the label that this agent will be called. (Usually this is a reversed version of your domain name, if you have one, and ending in something that identifies the particular task – in this case, a “test” task. But if you don’t have your own site, you could use anything, really, as the label.)

Opening the Contacts package

Next come the program arguments, which is an array (a kind of list) of text strings. The first item in that list (and there only needs to be one item in many cases) is either a Unix command or the path to the file to run… the agent, in other words. In this test case, I have provided the path to the Contacts app in Mountain Lion. Notice that you can’t simply give the path to the application… you have to give the path to the Unix executable file that is the core of the application! (Applications are actually folders, with a very specific structure, that pretend to be files.) If you control-click on the Contacts app (or any other app) in the Finder, you will see a menu with a Show Package Contents item. Choose it and you will be shown a window that reveals the inner contents of the application. In any application, there should be a Contents folder there, and inside that contents folder there will be a MacOS folder, and inside that there will be a Unix executable file. Generally, though not always, that executable file will have a name similar to that of the app, and it is that file that you want to specify as your agent.

Tip: Finding a file’s path

Writing out the path to a file in a way that Unix will understand is not an easy thing to do. Fortunately, there’s an easy cheat! Simply drop a file on the Terminal window, and the full Unix path to that file will be inserted. You can then easily copy that path to use elsewhere. In the picture below, the path starts with the ‘/’ that follows the Unix prompt (the part that ends with a ‘$’) and ends before the cursor (the gray block). Copy just that part and you’re golden! To be sure you understand what part is the path, compare with the path in the plist text above.

After the program arguments comes information about when the file will be executed. In this case, a StartInterval object is used, which simply specifies the number of seconds between each time  the agent runs. In this case, the agent will run every 300 seconds, or every 5 minutes.

Installing the agent

To see this agent in action, you simply have to put that net.reedcorner.test.plist file that you created earlier in the right place. (Also, if you are using a system older than Mountain Lion and don’t have the Contacts app, you should change the path in that plist file to a different app.)

Choose Go to Folder from the Finder’s Go menu and enter:

~/Library/LaunchAgents

This will open a window showing the contents of the LaunchAgents folder. Move the net.reedcorner.test.plist file that you created earlier into that folder. Then log out and log back in to activate the agent. Alternately, if you really don’t want to log out, you can execute the following command in the Terminal:

launchctl load -w ~/Library/LaunchAgents/com.reedcorner.test.plist

Once you have done either of those things, you should see that the Contacts app will open every 5 minutes. Of course, that will get annoying fast, so to disable the agent, simply remove that file from the LaunchAgents folder (put it back on your desktop again) and log out and back in again. Or, again, if you don’t want to log out, execute the following command in the Terminal before removing the file from the LaunchAgents folder:

launchctl unload -w ~/Library/LaunchAgents/com.reedcorner.test.plist

Running at a specific time

Sometimes you don’t want an agent to run every so many seconds, but at a particular time or on a particular day.  In that case, you would replace the StartInterval key, and the integer that follows it, with something like this:

   <key>StartCalendarInterval</key>
   <dict>
      <key>Minute</key>
      <integer>45</integer>
      <key>Hour</key>
      <integer>13</integer>
      <key>Day</key>
      <integer>7</integer>
   </dict>

In this example, the task would be run on the 7th day of every month at 1:45 pm. Here’s the meaning of the various keys that can appear in a StartCalendarInterval:

  • Minute
    A number from 0 to 59 that represents the minute on the clock at which the task will run.
  • Hour
    A number from 1 to 24 that represents the hour (in military time) at which the task will run.
  • Day
    A number from 1 to 31 that represents the day of the month on which the task will run.
  • Weekday
    A number from 0 to 7 that represents the day of the week on which the task will run (0 and 7 are both Sunday).
  • Month
    A number from 1 to 12 that represents the month in which the task will run.

These can be combined (with the exception of Day and Weekday), as shown above, but not all are required. In the example above, omission of the Month key means that it runs every month, on the specified day and at the specified time. If a Month key were added, it would run only once per year, on that day and time of the specified month. If only a minute were specified, the task would run at that minute, once per hour.

As another example, here’s a full plist file that opens the Contacts app on every Monday in June at 10:00 pm (don’t ask why that would be useful):

<?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>net.reedcorner.test</string>
   <key>ProgramArguments</key>
   <array>
      <string>/Applications/Contacts.app/Contents/MacOS/Contacts</string>
   </array>
   <key>StartCalendarInterval</key>
   <dict>
      <key>Minute</key>
      <integer>0</integer>
      <key>Hour</key>
      <integer>22</integer>
      <key>Weekday</key>
      <integer>1</integer>
      <key>Month</key>
      <integer>6</integer>
   </dict>
</dict>
</plist>

Creating custom agents

Just running an existing app is probably not the most useful behavior. You usually want something more customizable to happen. There are several ways to accomplish that.

First is to use Automator to create an application. Automator can help you easily create basic tasks, and the applications it creates are just like any other application. You would simply save it somewhere on your hard drive, view its package contents as mentioned previously, and get a path to the executable file (which will be called “Application Stub”) in the MacOS folder. Put that path in your plist file as the first item under ProgramArguments (ie, replace the path to the Contacts app in the examples above), install the plist file and you’re done.  Easy peasy!

Automator won’t do everything, though. Perhaps you have a more capable AppleScript that you want to run. In that case, you have two options. One is to simply create an Automator application that uses a Run AppleScript action to run your script. Of course, an Automator app involves a lot of overhead that can be avoided with a simpler AppleScript file. So you could also construct your ProgramArguments in the plist file to run the AppleScript file directly.  Suppose you have a script in a file named MyCoolScript.scpt, and that file is in your user folder. You could construct a plist file like this:

<?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>net.reedcorner.test</string>
   <key>ProgramArguments</key>
   <array>
      <string>osascript</string>
      <string>/Users/yourusername/MyCoolScript.scpt</string>
   </array>
   <key>StartInterval</key>
   <integer>300</integer>
</dict>
</plist>

This executes the Unix command osascript, passing it the path to the script file as a parameter, which results in running the script directly.

Another option is to run a Unix shell script. This option is for the geekiest among us, and chances are good that anyone with the expertise to write a shell script wouldn’t need to do more than skim this article. Still, if you have a shell script, or can write one, and you want to run it periodically, simply provide the path to that script file as the sole ProgramArguments string. Here’s a concrete example: create a new text file with the following contents:

#!/bin/bash
say "Peanut butter jelly time"

Now save that file to your user folder, naming it “AnnoyMe.sh”. Then execute the following command in the Terminal (being sure to replace the path with the proper path on your system):

chmod a+x /Users/yourusername/AnnoyMe.sh

Next, create the following plist file (again replacing the path with the correct one for your system) and install it as a LaunchAgent:

<?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>net.reedcorner.test</string>
   <key>ProgramArguments</key>
   <array>
      <string>/Users/yourusername/AnnoyMe.sh</string>
   </array>
   <key>StartInterval</key>
   <integer>10</integer>
</dict>
</plist>
 Be forewarned, this script will do exactly what it says it will do! (Specifically, annoy you by saying “Peanut butter jelly time” every 10 seconds.)

3 Comments

  • Al Varnell says:

    Surprised you didn’t mention Peter Borg’s Lingon app http://www.peterborgapps.com/lingon/index.html as an inexpensive and much less geeky way to write or edit launchagents / launchdaemons.

    • Thomas says:

      I wasn’t aware Lingon was still around! Last time I was aware of it (which was some time ago), I seem to recall that it hadn’t been updated for a very long time and it no longer worked with the latest version of the OS. Thus it slipped off my radar. I guess that changed, though. Now I feel a bit silly for posting the manual instructions. Still, I suppose my effort wasn’t wasted, as there are definitely folks out there who would prefer to do things manually, so that they understand fully what’s going on, when possible.

      In any case, thanks for mentioning it!

  • Alex says:

    Been trying to find a way to schedule a recurring restart script that would run regardless of who was logged in. I took a look at Lingon, but I don’t think it has the ability to run as another user. Followed most of your instructions, but shoved the script into /Library/LaunchDaemons as root instead of ~/Library/LaunchAgents as user. The restart runs regardless of who’s logged in now. Awesome post….thanks!

This post is more than 90 days old and has been locked. No further comments are allowed.