notes from /dev/null

by Charles Choi 최민수


Scheduling Future Tasks in Emacs

24 Jun 2026  Charles Choi

Back when I had to run a lot of long-running simulations on Unix systems, I became an ardent user of the at command, which lets one schedule a program to run at a future date. I also used at to schedule different notifications, like sending a message to a pager (yer, how quaint).

Moving to macOS (nee OS X) changed that, as it disabled by default the at command due to battery life management. As I was unwilling to give up battery life for at, I learned to live without it. That was, until I learned that Emacs had run-at-time.

The run-at-time command does as it says on the tin: it will run a function at some future specification of time.

1
(run-at-time TIME REPEAT FUNCTION &rest ARGS)

That specification of time is “natural language-like” in that multiple string representations of time can be accepted, both absolute and relative.

The function can be anything Elisp offers, but running shell commands is what makes run-at-time a substitute for at, with some caveats.

Jobs that are submitted by run-at-time can be listed out via the list-timers command. You can cancel a job (actually a timer object) using the c key, bound to timer-list-cancel. Note that Emacs disables list-timers by default as it exposes timers that you should likely not mess with. Be forewarned.

Another caveat is that using run-at-time presumes that your Emacs session is still running at that future time. If you have Emacs running all the time (particularly as a server) then this should work without complication.

Example: Scheduling a macOS Notification

A common use I have for run-at-time is to send myself a notification at a future time. The way I prefer to be notified is using the macOS Notification Center. Here’s an example of what a notification looks like:

The above notification was sent using a macOS Shortcut I’ve defined named nota. Shown below is how nota is defined in the Shortcuts app.

A macOS shortcut can be invoked from the command line. We can wrap this capability in the Elisp function cc/run-nota.

1
2
3
4
5
6
7
8
(defun cc/run-nota (msg)
  "Run nota shortcut with MSG."
  (process-lines
   "shortcuts"
   "run"
   "nota"
   "-i"
   msg))

Finally we can schedule cc/run-nota with run-at-time through the wrapper command cc/notify-at.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
(defun cc/notify-at (&optional start-time msg)
  "Send a nota notification with MSG at START-TIME.

This command invokes `cc/run-nota' with MSG at START-TIME passed into
`run-at-time'."
  (interactive)
  (let* ((start-time (if start-time
                         start-time
                       (read-string "Time: ")))
         (msg (if msg
                  msg
                (read-string "Message: "))))
    (run-at-time start-time
                 nil
                 #'cc/run-nota
                 msg)

    (message "Show notification “%s” in/at %s" msg start-time)))

Users wishing to install the nota macOS shortcut can download it via the following iCloud link:

nota shortcut (via iCloud)

Closing Thoughts

run-at-time is an underappreciated built-in which has given me much of what I used to use at for. If you use Emacs for implementing any kind of automation, you should include run-at-time as part of your toolbox for it.

emacs   macos

 

AboutMastodonBlueskyGitHub

Feeds & Tags
Get Scrim for macOSGet Captee for macOS

Powered by Pelican