"A wait that is too short does not report a failure, it manufactures one."
Two Ways to Start at Login
A Mac app a person turns on for themselves ("Launch at login" in Settings) should use SMAppService.mainApp.register(). The system lists it under Login Items, and the user can switch it off there. The family's always-on utilities (the text-transformation tool, the dictation tool, the voice launcher) are deployed to every Mac by a script instead, and they need more than a login item gives: restart after a crash, a log file, and a job the deploy script can restart by name. Those ship as a LaunchAgent, a property list in ~/Library/LaunchAgents that launchd runs in the user's GUI session.
The Keys That Matter
RunAtLoad starts the job when the agent loads, which includes every GUI login. LimitLoadToSessionType = Aqua ties it to the graphical session, which a menu-bar app needs. ProcessType = Interactive tells launchd the process serves the user directly, so it runs with an app's resource limits instead of the CPU and I/O throttling an unclassified job gets. StandardOutPath and StandardErrorPath give the app a log that exists before any logging code runs. The key worth reading twice is KeepAlive = { SuccessfulExit = false }: launchd restarts the job only when it exits unsuccessfully. Measured on a scratch agent, exit 3 was respawned (state = spawn scheduled, then running again), while exit 0 stayed not running. A user who chooses Quit gets exit 0, so the app stays quit, and a crash brings it back. A plain KeepAlive = true would resurrect the app every time the user quits it.
Three Deploy Traps, All From One Family
Killing first respawns the old binary. An install script ran pkill before swift build. launchd owned the app, so it immediately restarted it from the still-old bundle while the build ran, and replacing the bundle later changed the disk but not the running image. The working order: keep the current process alive while building and signing, install the complete signed bundle by rename, verify it, and only then launchctl kickstart -k the loaded job.
A short wait invents a failure. Right after a bundle swap, launchd may spawn the job, fail code-signing evaluation once (last exit reason = OS_REASON_CODESIGNING) and retry. A script that polled for 5 seconds printed HARD_FAIL for an install that was correct, and the natural response, re-running the install, perturbs a system that was already fine. The wait is now 20 seconds, and the failure message says the bundle is installed.
KeepAlive turns a failing precondition into a loop. A service's runner script checked, under set -e, that a path outside the service existed. The path went away days later, and nothing noticed, because the running process had started before the check broke. The next restart would fail the check, exit, be restarted, and fail again forever. A runner should assert only what the service itself needs, like its interpreter and its data folder, and a fix is proven only when the serving pid started after the change.