Schedule Tasks with Crontab and Launchd

Schedule Tasks with Crontab and Launchd

When we write a lot of scripts we will eventually want to automate some tasks like saving Database backups, scrap a website periodically, do POST or GET requests to an API, etc. In all these cases we will need a task scheduler. On a Linux systems, our easiest and best option is Crontbab and while it is a great option for quickly creating a scheduled task both on Linux and Mac, Apple deprecated it in favor of launchd. This does not mean that we can’t use Crontab on Mac but the downside of Crontab is that is assumes your machine is awake and therefor will skip the task until the next time if this is not the case. On the other hand, launchd will not run while your Mac is asleep BUT once you wake it up again, it will run.

Nonetheless, both are great and thus we will cover them both. These are the main reasons we would want to use Crontab over launchd :

  • It is simpler to setup
  • We don’t care if the job runs every time, it just should run on a best effort basis

Index:

Crontab

As mentioned above Crontab is simpler to setup, it comes already installed and all we have to do is add our job to the list which we can access using the -l flag:

$ crontab -l

This lists all our jobs which currently is probably empty. In order to add a job we use the -e flag:

$ crontab -e

This will let us add a job to the list. The default editor on Mac is VIM but don’t be afraid. Here is a very quick guide to VIM. When the VIM editor opens:

  • Use the i key to start insert mode and edit the file
  • Use the arrow keys to move around the cursor
  • Use the esc key to stop editing
  • Type :wq to write your changes and exit after the esc key

Now lets get into the actual job running:

$ 1 2 3 4 5  node  my_node_job.js

Crontab lets you specify the time by putting your desired times in the right slots as followed (based on the above job):

  • 1) minute (0 - 59)
  • 2) hour (0 - 23)
  • 3) day of month (1 - 31)
  • 4) month (1 - 12)
  • 5) day of the week (0 - 6)

For example a tasks that will run every day at 4pm:

0 16 * * *  node  my_node_job.js

If you don’t specify a day, month or day of the week, it will run every day. If we take a look at our job list again, we can see our newly created job:

$ crontab -l
0 16 * * * node  my_node_job.js

A little extra to note, if we leave the cronjob as it is, we will get an email in our terminal every time it executes which can be a little annoying. The fix is fairly simple, open the job list again and add at the top this line:

MAILTO=""
0 16 * * *  node  my_node_job.js

No more email great ^^

Launchd

While Crontab is a quick solution for scripts that don’t necessarily have to run every day, sometimes we need something more robust. Sometimes we might be commuting at the time our job should run and as mentioned above, Crontab will skip this job and will not run it until tomorrow at the same time. Launchd solves this problem for us because it knows we are not always actively using our computer.

In order to create a new job with Launchd we have to create our .plist file first which is just an XML file with some boilerplate XML from Apple’s docs. We can create the file using touch:

$ touch ~/Library/LaunchAgents/org.USERNAME.TASK-NAME.plist

Using the Crontab example, note my username is Alexander you have to use yours:

$ touch ~/Library/LaunchAgents/org.Alexander.my-node-job.plist

Now we can open it in our favorite editor (mine is Atom):

$ atom ~/Library/LaunchAgents/org.Alexander.my-node-job.plist

This is the boilerplate XML we need for any job:

<?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>org.Alexander.my-node-job</string>
    <key>ProgramArguments</key>
    <array>
        <string>/Users/alexander/.nvm/versions/node/v8.4.0/bin/node</string>
        <string>/Users/alexander/Desktop/my-node-job.js</string>
    </array>
</dict>
</plist>

At this point we have two options to schedule our job:

  1. Run it in intervals (every 10 seconds, 1 minute, etc.)
  2. Run it a specific times (4pm every day)

If we want to run it in intervals we have to add an interval key to our XML specifying the interval in seconds 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>org.Alexander.my-node-job</string>
    <key>ProgramArguments</key>
    <array>
        <string>/Users/alexander/.nvm/versions/node/v8.4.0/bin/node</string>
        <string>/Users/alexander/Desktop/my-node-job.js</string>
    </array>
    <key>StartInterval</key>
    <integer>60</integer> <!-- 60 seconds or 1 minute -->
</dict>
</plist>

This would run every 60 seconds. If we want to run our job based on a specific time we exchange the interval key for a calendar interval key:

<key>StartCalendarInterval</key>
    <dict>
        <key>Weekday</key>
        <integer>5</integer>
        <key>Hour</key>
        <integer>5</integer>
        <key>Minute</key>
        <integer>5</integer>
    </dict>

In our case all we want is to specify the hour since we want to run our job every day at the same time:

<?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>
    <!-- The label should be the same as the filename without the extension -->
    <string>org.Alexander.my-node-job</string>
    <key>ProgramArguments</key>
    <array>
        <string>/Users/alexander/.nvm/versions/node/v8.4.0/bin/node</string>
        <string>/Users/alexander/Desktop/javascript_box/nodejs/cronjob.js</string>
    </array>
    <key>StartCalendarInterval</key>
    <dict>
        <key>Hour</key>
        <integer>16</integer>
    </dict>
</dict>
</plist>

Great now all that is left is to load our agent (job). Here you have 2 options, you can do it the manual way or use a small tool for this called lunchy:

The Manual way

We have to use launchctl to load and start our job (note, replace Alexander with your username):

$ launchctl load ~/Library/LaunchAgents/org.Alexander.my-node-job.plist
$ launchctl start org.Alexander.my-node-job

Using Lunchy

Lunchy is a Ruby gem so we have to install it first like this:

$ gem install lunchy

Once it is downloaded we can load our job a little more simply, we don’t have to specify a path nor the username just the plist’s name:

$ lunchy restart my-node-job

Similarly to list our jobs:

$ launchctl list
$ lunchy list

Finally to stop our Agent:

$ launchctl stop org.Alexander.my-node-job
$ lunchy stop my-node-job