macOS login items are all-or-nothing. Every app you’ve added fires at the same moment — which means they all compete for CPU and disk at the worst possible time. Slower apps fail to open properly, others show up with missing state, and your machine takes longer to settle than it should.
The obvious fix is to spread the launches out. Turns out there’s no built-in way to do that.
What Staggered does
You add apps to a list, set a delay in seconds for each one, and toggle Run at Login. That’s it. From that point, every time you log in, Staggered wakes up silently in the background, waits the right amount of time for each app, opens them in order, then quits without ever showing a window or a dock icon.
The GUI is only there when you open it yourself — from Finder, Dock, or Spotlight. It knows the difference based on whether it was launched with a --login argument, which the LaunchAgent plist passes explicitly. There’s no guesswork involved.
Two timing modes
Sequence mode stacks delays cumulatively. If you set Slack to 5s and Spotify to 8s, Slack launches at t+5s and Spotify at t+13s (5 + 8). Each app waits for the previous gap to close.
Parallel mode runs all timers simultaneously from boot. The same settings would give you Slack at t+5s and Spotify at t+8s, independently. Neither waits for the other.
Parallel is useful when your apps don’t depend on each other and you just want to spread the load. Sequence is better when launch order actually matters — for example if one app needs to be running before another connects to it.
How the login detection works
The bit I spent the most time getting right was reliably telling the difference between a login launch and a user launch. A few approaches turned out to be wrong.
Checking the parent process ID seemed like a good idea — login items launched by launchd should have PID 1 as their parent. Except on modern macOS, apps opened from the Dock are also ultimately parented to launchd, so that check fires for both cases.
SMAppService.mainApp is the modern API for login item registration, but it doesn’t support passing custom arguments. So you can register the app, but you can’t tell it anything about why it’s launching.
The solution was to skip SMAppService entirely and write a LaunchAgent plist directly to ~/Library/LaunchAgents/. This gives full control over ProgramArguments, so the plist explicitly passes --login. When the app sees that flag it runs headless; when it doesn’t, it opens the configurator. Simple and unambiguous.
Building it
Staggered is built with SwiftUI and AppKit and has no external dependencies. The build is a single build.sh script using swiftc directly — no Xcode project file needed.
chmod +x build.sh
./build.sh
cp -r "build/Staggered.app" ~/Applications/
open ~/Applications/Staggered.app
Move it to a permanent location before enabling Run at Login, since the LaunchAgent plist stores an absolute path to the executable. If you move it afterwards, just toggle the login item off and back on to rewrite the plist.
Logs from login launches go to /tmp/staggered.log if anything needs debugging.
If this app seems useful to you, then the code is available on GitHub - Staggered App.