You can generate a plist. Now you need to get launchd to load it. This is the installJob function — the bridge between your job definition and the OS.
The Lifecycle
Every install follows four steps:
- Write the plist to
~/Library/LaunchAgents/{label}.plist - Bootout the old registration (if any) —
launchctl bootout gui/{uid} {plist} - Bootstrap the new registration —
launchctl bootstrap gui/{uid} {plist} - Enable it —
launchctl enable gui/{uid}/{label}
This sequence is important. If you bootstrap without booting out first, you get "service already loaded." If you skip enable, the job might not start on schedule.
Try It Manually
Uninstall a job, then reinstall it step by step:
# Uninstall
bun run src/cli.ts uninstall hello-world
# Verify it's gone
launchctl list | grep aijs.system.hello-world
# (no output)
# Reinstall
bun run src/cli.ts install hello-world
# Verify it's back
launchctl list | grep aijs.system.hello-world
# - 0 aijs.system.hello-world
The install command shows the before/after state:
{
"result": {
"name": "hello-world",
"wasLoaded": false,
"nowLoaded": true
}
}
The gui/ Domain
launchd organizes jobs into domains. Your user-level jobs live in gui/{uid}:
echo "Your domain: gui/$(id -u)"
# gui/501
This means your jobs run as YOUR user, no root required. They have access to your files, your environment, your network. They start when you log in and stop when you log out.
What Can Go Wrong
bun run src/cli.ts doctor hello-world
The doctor checks for the common failures:
- Plist not installed — run sync or install
- Plist fails validation — bad XML, check the generator
- Loaded but non-zero exit — the job ran and errored, check logs
- Run script not executable — needs
chmod 755 - No shebang — launchd doesn't know how to run it
File Permissions Matter
- Run script: must be
755(executable). The manager doeschmod 755automatically. - Plist file: must be
644. Launchd ignores plists with group/other write permissions. The manager writes with{ mode: 0o644 }.
Verification
# Full round trip: uninstall → verify gone → install → verify running → kick → check logs
bun run src/cli.ts uninstall hello-world
launchctl list | grep hello-world # should be empty
bun run src/cli.ts install hello-world
launchctl list | grep hello-world # should show the job
bun run src/cli.ts kick hello-world
bun run src/cli.ts logs hello-world # should show new output
What You Learned
- The install sequence: write → bootout → bootstrap → enable
- Jobs live in
gui/{uid}— user-level, no root - File permissions matter: 755 for scripts, 644 for plists
jobs doctorcatches the common failures before you debug manually