Skip to content

On your machine ​

sh
awaywork schedule install                 # macOS: launchd
awaywork schedule install --with systemd  # Linux: a user service
awaywork schedule install --with cron
awaywork schedule status                  # the job, the daemon and its log
awaywork schedule remove

The job is awaywork watch, kept alive by the scheduler. It checks the clock every minute and starts a run when a window opens, even after the machine slept through the opening, and rereads the config each time, so window edits need no reinstall. It also runs run: soon rules within minutes.

SchedulerWritesKeeps it alive
launchd~/Library/LaunchAgents/awaywork.plistStarted at login, restarted after a crash (KeepAlive, SuccessfulExit: false)
systemd~/.config/systemd/user/awaywork.serviceEnabled at login, Restart=on-failure
crontwo tagged lines in your crontabStarted at boot (@reboot) and again every 5 minutes
$ awaywork schedule status
Scheduled with launchd, keeping awaywork watch running
Config: ~/.config/awaywork/config.yml
Log: ~/.local/state/awaywork/schedule.log
Daemon: running, pid 67014, since 2026-10-25 17:26:00
Run in progress: soon run 20261025-162625
Next poll of flaky: 2026-10-25 17:30:00, backing off after 1 failed poll
Next window: none, no windows set
Daemon log: ~/.local/state/awaywork/watch.log

The daemon's log, watch.log, has events only: start and stop, runs and units starting and ending, and errors; past 5 MB it moves to watch.log.1. A run's full output goes to run.log in its run dir, and into watch.log too with watch: { debug: true }. schedule.log catches only a crashed daemon. The morning report is awaywork report. Both list what failed before any unit started:

warning: source chores: Command failed with exit code 1: ./chores.sh
  Error: CHORES_TOKEN is not set

Details:

  • One opening, one run. A run that finishes early isn't restarted in the window; one started inside a window, after a reboot say, picks up there, skipping items already worked on.
  • One daemon at a time: another copy exits with awaywork watch is already running (pid 67014), which makes cron's restart line harmless.
  • Your run or the daemon's. A manual awaywork run during a daemon run exits 1 (awaywork watch holds the run lock (pid 67014); try again once its runs end, awaywork schedule status lists them). A window opening during your manual run waits for it, checking every minute and logging it once; a soon run waits for the next poll. A dead process's lock is taken over; --dry-run takes none.
  • PATH: the job keeps the installing shell's, so it finds the same claude, codex, agent and gh, but runs the binaries, not shell functions: pin logins in the config instead.
  • Secrets come from ~/.config/awaywork/env. The daemon reads it and its own code once, at start: after editing it or upgrading, run schedule install again, which restarts it.
  • Restarting: install and remove stop the running daemon first, whoever started it: a SIGTERM, then up to 2 minutes. A running unit stops, publishes nothing and isn't counted as an attempt. If it's still running after that, install changes nothing and says so. Run from inside the daemon (a hook, a worker), they refuse.
  • launchd and systemd restart it only after a crash; a SIGTERM of yours keeps it down until your next login or install. Under cron it's back within 5 minutes.
  • systemd user services run only while you're logged in (loginctl enable-linger lifts that). Without a user session (sudo -iu, su -) --with systemd fails and changes nothing, and status and doctor report no schedule; log in directly or use cron.
  • windows: [] is fine: the daemon idles and install sets no wake.
  • A failed command gets two lines in the log: its error and the last error line of its stderr. Configured secrets show as ***, so the report is safe for notify. Run ids are the start time in UTC.
  • A job from an older awaywork (one trigger per window running awaywork run --scheduled) still works, but status and doctor flag it: run schedule install once.

Soon runs ​

Every watch.every (5 minutes), the daemon polls the sources its run: soon rules read, plans just those rules and starts a run for the units due: new items, or ones changed since their last attempt, like a PR gone red or your reply to awaywork's questions. Here a source's item came in, awaywork's questions on it didn't count, the reply did, and a second source was down for a while:

2026-10-25 16:17:51 awaywork watch started, pid 27290, config ~/.config/awaywork/config.yml
2026-10-25 16:17:51 source flaky: poll failed, next at 2026-10-25 16:21:00: Command failed with exit code 1: ./flaky.sh
2026-10-25 16:17:51 soon run 20261025-151751 started for ch-1
[...]
2026-10-25 16:21:00 source flaky: polled fine again, every 2m
2026-10-25 16:22:00 soon run 20261025-152200 started for ch-1

Details:

  • awaywork's own questions comment isn't a change, nor is anything a run already took: a failed unit waits for the item to change, or the next window. One its run never attempted (no quota, sat out a present run, stopped) is due again an hour later, or at the first poll after a restart.
  • A clock-driven job, Mondays at 09:00, is a routine, which a soon rule starts at the first poll after.
  • Each poll that finds units makes one run, with its own run dir, report and per_run share. One that finds nothing writes nothing (one line with debug).
  • Outside a window a soon run is present: workers stop at present_session_max and providers without a quota reading sit out. Inside one it's away and ends with the window.
  • A poll reads no quota and runs no model. A GitHub poll is 3 searches (your PRs, review requests, assigned issues), one per search entry and two more for finished items: about 36 an hour at 5 minutes. A tracker poll is its list call plus a PR search per open task without a link. Give a slow or rate-limited tracker its own every.
  • A failed poll backs off as long as Retry-After says (Jira and Asana, on a 429), else doubling per failure up to an hour, logging each backoff and recovery. GitHub, Linear and custom sources give no reset time, so they double.
  • notify gets a soon run's unit events, not the run's end. A unit that sat out or was passed over by when or quota fires none, so hourly retries stay quiet; a failed setup or a timed-out when still fires.
  • Runs overlap, never on one item, and one worker runs at a time: a soon unit waits for the window run's current chain, not the whole run, routing on the quota it read when planning. A unit another run holds is skipped as running in run 20261025-010000. The daemon holds the run lock, and keeps the machine awake, while any run is in progress.
  • An isolation edit applies to the next run that starts with none in progress, logged once while it waits. Restart watch or rerun schedule install to apply it now.

Waking up for it ​

A sleeping machine runs nothing. While a run is in progress the daemon keeps the machine from idling to sleep (caffeinate -i on macOS, systemd-inhibit --what=idle on Linux where it works); the wake itself is a separate step schedule install offers in a terminal:

◆  Wake this Mac when the window opens (sudo pmset repeat wake MTWRFSU 01:00:00)?
│  ● Yes / ○ No
└
  • macOS: pmset keeps one repeating wake, so every window must start at the same time, and it replaces your other repeating power events, scheduled sleeps and shutdowns too: the question lists them and defaults to No unless all it replaces is a wake. Keep a laptop on the charger; lid closed on battery, it may not wake fully.
  • Linux: a root timer, /etc/systemd/system/awaywork-wake.timer with WakeSystem=true. A background job can block idle but not sleep, so turn off a desktop's own suspend timer on AC power.
  • sudo needs a password, so without a terminal install only says how. schedule remove leaves the wake and prints how to remove it.
  • Moving the windows' start needs schedule install again to move the wake; doctor warns until then.