macOS LaunchAgents
mise can declare macOS user LaunchAgents in [bootstrap.macos.launchd.agents] and apply them with mise bootstrap macos launchd-agents apply or as part of mise bootstrap:
Run this as the user who owns the agent in a macOS session with a GUI launchd domain. Create the executable and any log directories before applying it. The example's my-sync is a placeholder for your own program:
[bootstrap.macos.launchd.agents.my-sync]
program = "~/.local/bin/my-sync"
args = ["--watch"]
run_at_load = true
environment = { PATH = "/opt/homebrew/bin:/usr/bin:/bin" }
working_directory = "~"
stdout_path = "~/Library/Logs/my-sync.log"
stderr_path = "~/Library/Logs/my-sync.err.log"Each agent is written to ~/Library/LaunchAgents/dev.mise.<name>.plist and loaded with launchctl bootstrap gui/$UID ~/Library/LaunchAgents/dev.mise.<name>.plist. Agent names may contain letters, numbers, ., _, and -. mise owns only the plist files it creates with the dev.mise. label prefix.
The agent receives launchd's environment, not your interactive shell's activation. Use explicit executable paths and declare required environment variables. program and args form an argument vector; shell expressions such as pipes and redirections need an explicitly invoked shell or a wrapper script.
Supported keys
| TOML key | launchd key |
|---|---|
program | ProgramArguments[0] |
args | ProgramArguments[1..] |
run_at_load | RunAtLoad |
keep_alive | KeepAlive |
keep_alive_on_failure | KeepAlive = { SuccessfulExit = false } |
start_interval | StartInterval |
throttle_interval | ThrottleInterval |
process_type | ProcessType |
start_calendar_interval | StartCalendarInterval |
queue_directories | QueueDirectories |
environment | EnvironmentVariables |
working_directory | WorkingDirectory |
stdout_path | StandardOutPath |
stderr_path | StandardErrorPath |
process_type is the scheduling band launchd puts the job in: Background, Standard, Adaptive or Interactive. Background is the usual choice for work that should yield to the user, since launchd throttles CPU and disk I/O for that band. Write it exactly as shown, which is launchd's own spelling and what the JSON schema validates against; anything else is an error, because launchd ignores a ProcessType it does not recognize and the job would silently run in the default band. A value that is only miscapitalized is still rejected, with the right spelling named in the message.
keep_alive and keep_alive_on_failure are mutually exclusive. Set only one: the former keeps the process running after any exit, while the latter restarts it only after a failure. | kickstart | run launchctl kickstart |
program, working_directory, stdout_path, stderr_path, and each entry in queue_directories expand bare ~ and ~/ to the current user's home directory before writing the plist. args are passed through exactly as written. start_calendar_interval accepts minute (0-59), hour (0-23), day (1-31), weekday (0-7), and month (1-12), and writes the corresponding launchd calendar keys. For multiple independent calendar schedules, use an array of inline tables:
[bootstrap.macos.launchd.agents.daily-sync]
program = "~/.local/bin/my-sync"
start_calendar_interval = [{ hour = 3 }, { hour = 12, weekday = 1 }]start_interval and start_calendar_interval are independent launchd triggers. If both are set, launchd can start the agent from either schedule.
throttle_interval is the minimum number of seconds launchd waits between runs of the agent (launchd's default is 10).
queue_directories starts the agent whenever any of the listed directories is non-empty; launchd expects the agent to drain them. launchd requires absolute paths here, so each entry must start with /, or be ~ or a ~/ path that expands to one.
Templates
Agent values are rendered as Tera templates before the plist is written, using the template context of the config file that declared the agent:
[bootstrap.macos.launchd.agents.my-sync]
program = "{{ config_root }}/bin/sync"
working_directory = "{{ config_root }}"
stdout_path = "{{ config_root }}/log/sync.log"Every string value is rendered, including entries inside args, environment, and queue_directories. A value with no template syntax skips the renderer unchanged; the ~ expansion described above still applies to it afterwards. {{ exec(...) }} is not available here, for the same reason as in systemd units. An agent whose template fails to render is reported and skipped; other agents still apply.
Semantics
- Declarative and additive — agent names merge across the config hierarchy (global → project). A more local config replaces the full declaration for the same agent name.
- macOS-only — on other platforms the section is inert:
mise bootstrap macos launchd-agents statuslists entries as skipped andmise bootstrap macos launchd-agents applyignores them. - Manual application only — mise never writes or loads LaunchAgents implicitly; only
mise bootstrap macos launchd-agents applyandmise bootstrapdo. - User agents only — mise writes to
~/Library/LaunchAgents. System daemons in/Library/LaunchDaemonsare not supported.
Commands
mise bootstrap macos launchd-agents status # shows LaunchAgent state
mise bootstrap macos launchd-agents status --json # machine-readable
mise bootstrap macos launchd-agents status --missing # exit 1 if any agent is missing, changed, or unloaded
mise bootstrap macos launchd-agents apply # write and load missing/changed agents
mise bootstrap macos launchd-agents apply --dry-run # print the commands without running them
mise bootstrap macos launchd-agents apply --yes # skip the confirmation promptstatus reports each agent as loaded, unloaded, differs, or missing. apply rewrites changed plists, unloads the old job if present, loads the new job, enables it, and runs kickstart only when kickstart = true.