| #journal, #shellscript, #devtool | |||
Job Scheduling with at(1)The Unix utility that knows when teatime is |
|||
I recently hacked together an ESP-01 and a cheap solar garden lantern1 so that I can have warm flickering candlelight whenever the mood strikes. For example, by using at(1), I can make teatime2 just a little more magical:
$ echo "curl 'http://ember.local/on'" | at teatime
job 42 at Sun Feb 22 16:00:00 2026
at(1) is a Unix utility for scheduling one-off commands. In the example above, a request to turn on the lantern will be sent at 16:00 local time.
You can verify it’s queued with atq, and view the job details with at -c [job]:
$ atq
42 Sun Feb 22 16:00:00 2026
$ at -c 42
#!/bin/sh
# atrun uid=502 gid=20
# ...
curl 'http://ember.local/on'
Whereas cron(8) is used for scheduling recurring jobs, at is used for scheduling one-off tasks. For example, here are some ways I’ve found it useful:
Reminding myself to take a break
$ at now + 1 minute <<'EOF'
osascript -e "display dialog \"Stop what you're doing!\" with title \"Take a break\""
EOF
Posting fresh data before a team sync
$ echo "curl https://api.example.com/stats | jq . | ~/pipe-to-slack.sh" | at 9:15AM tomorrow
Stopping that debug container I’m probably going to forget about
$ echo "docker stop my-debug" | at 1700 friday
Although I haven’t tried it myself, you can even implement “recurring” jobs by recursively rescheduling the next run at the end of the current job. That said, if such a job fails, the chain breaks silently; there is no built-in retry or alerting like cron provides.
$ cat ~/daily.sh
#!/bin/bash
# Do some work here...
at -f ~/daily.sh 5pm tomorrow
$ at -f ~/daily.sh 5pm
job 43 at Mon Feb 23 17:00:00 2026
This example uses the -f flag to tell at to read the job from a file.
After a job is executed, its output will be captured and sent to you via local sendmail(8)3. On macOS, mail lands in /var/mail/$USER by default, and you can read it with mail(1).
Keep in mind that at snapshots your exported environment (working directory, env vars, umask) from wherever you schedule a job, but it does not capture your shell profile (e.g. anything from .bashrc, .zshrc, etc.). Therefore, it’s common practice to use absolute paths and avoid shell-specific features.
Quick Start
To get started using at on macOS, you’ll need to manually enable the atrun(8) daemon using launchctl(1):
$ sudo launchctl load -w /System/Library/LaunchDaemons/com.apple.atrun.plist
Command reference:
at <time>- schedules a job
atq- lists pending jobs
at -c <id>- shows what a job will run
atrm <id>- cancels a job
at -f script.sh 3pm- reads from a file instead of stdin
at -m- sends mail even if there’s no output
-
Like this: Lampioncino_solare.jpg, Antonia Mette, CC BY-SA 4.0, via Wikimedia Commons ↩
-
“…the following keywords may be specified: midnight, noon, or teatime (4pm)…” at(1) - POSIX specification ↩
-
By default,
atonly sends mail if a job produces output. If you want to receive mail even when there’s no output, you can use the-mflag. ↩