# Running on a Schedule Installing the daily run on Linux and Windows, what to know before relying on it, and how results reach your phone. ← [README](../README.md) · [Providers and Tokens](providers.md) · [CLI Reference](cli.md) · [Troubleshooting](troubleshooting.md) --- ## Scheduling a Daily Run `sync` chains `export` then `joplin` in one invocation, which is what a scheduler wants — one command, and a meaningful exit code so a failed run is visible instead of silent. ```bash ./ai-chat-exporter sync # every configured provider ./ai-chat-exporter sync --provider codex # just one ``` ### Linux (systemd user timer) ```bash ./scheduling/install-systemd-timer.sh --provider claude-code --provider codex ./scheduling/install-systemd-timer.sh --uninstall ``` Defaults to 09:00 daily (`--time 21:30` to change). `Persistent=true` means a machine that was off at the scheduled time runs the archive at next boot rather than skipping the day. To archive while logged out, `loginctl enable-linger $USER`. Check on it with `systemctl --user list-timers aichat-sync.timer` and `journalctl --user -u aichat-sync.service -n 50`. ### Windows (Task Scheduler) ```powershell .\scheduling\Register-AiChatSyncTask.ps1 -Provider chatgpt,claude .\scheduling\Register-AiChatSyncTask.ps1 -Unregister ``` Per-user task, no admin rights needed. `-StartWhenAvailable` is the counterpart of systemd's `Persistent=true`. The task runs `scheduling\run-sync.ps1` with its window hidden; Task Scheduler keeps no output, so each run's output is appended to `cache\logs\scheduled-sync.log` (rolled over to `.1` past 1 MB). Check on it with `Get-ScheduledTaskInfo -TaskName AiChatExporterSync` (look at `LastTaskResult`: 0 is success) and `Get-Content cache\logs\scheduled-sync.log -Tail 50`. ### What to know before you rely on it **List only the providers that work unattended on that machine.** The local providers (`claude-code`, `codex`) need no credentials and always work. The web providers depend on a session token that expires and can only be refreshed by hand via DevTools — so on a machine where that token is stale, scheduling them means a failed run every single day, which is a good way to learn to ignore failures you actually want to see. That is why `--provider` is repeatable in both installers: schedule the coding machine for `claude-code` + `codex`, the browser machine for `chatgpt` + `claude`. **Both installers pass `--joplin-optional`.** If Joplin desktop isn't running, the sync warns instead of failing: the export has already captured the local transcripts (the part that can disappear), and the notes are rebuilt from the cache on the next run that finds Joplin up. **One provider failing does not skip the rest.** Both installers loop over the providers in a single action rather than one action each, because systemd `oneshot` stops at the first failing `ExecStart` and Task Scheduler reports only the last action's result. Every provider is attempted; the run still exits non-zero if any failed. The loop is `scheduling/run-sync.sh` on Linux (the unit's `ExecStart`) and `scheduling\run-sync.ps1` on Windows (the task's action). ### Getting notified A scheduled run is silent by default. Output goes to three pull-only places: the exporter's own log (`cache/logs/exporter.log`), the systemd journal on Linux (`journalctl --user -u aichat-sync.service`), and on Windows Task Scheduler's `LastTaskResult` plus `cache\logs\scheduled-sync.log`. To have runs report back, set an [ntfy](https://ntfy.sh) topic in `.env`: ```bash NTFY_TOPIC=my-archive-topic ``` Then check it works before waiting on a scheduled run: ```bash ai-chat-exporter notify # show settings ai-chat-exporter notify --test # send a test push ``` `sync` pushes a result whenever a topic is configured — a success carries the per-provider counts at low/default priority, a failure carries the reason at high priority with an alert tag, so a failed archive is distinguishable from a quiet one on your phone. `NTFY_NOTIFY=failure` notifies only on failure; `off` disables it; `--notify` / `--no-notify` override per run. `sync` can only push from the end of a run it finished. A crash, an exit before the sync starts (the ToS gate, a cache error) or a launcher that can't build its venv sends nothing — and since each provider pushes separately, the ones that succeeded still say "OK", so a dead provider looks like a quiet day. The scheduler wrappers (`scheduling/run-sync.sh`, `scheduling\run-sync.ps1`) close that gap: any run that exits non-zero without the app having reported it gets a high-priority **FAILED** push naming the provider and, for a crash, the exception's class (`claude-code: crashed (RecursionError)`) — the class only, never its message, which can carry a conversation title. The traceback is in the journal on Linux and in `cache\logs\scheduled-sync.log` on Windows. The message includes the **machine name**, which matters because both machines archive into one topic. It contains counts only — never conversation titles. A topic on public ntfy.sh is readable by anyone who knows its name, so if you want it private, self-host (`NTFY_SERVER`) or use an access-controlled topic with `NTFY_TOKEN`. A notification is never fatal: if ntfy is unreachable, the run logs a warning and still reports its real exit code. **Acknowledge the ToS notice once, interactively.** It's stored in the cache manifest per machine. Until then a scheduled run exits 1 with an explanation rather than hanging on a prompt no one can answer.