Run LatchAI as a service
Triggers only fire while the engine is running. Running
npm run engine in a terminal is fine while you’re working, but an automation that is
supposed to fire at 3am needs the daemon to outlive your terminal, your logout, and the
occasional crash. On macOS that means launchd.
Install
Section titled “Install”The engine CLI writes and loads the LaunchAgent for you:
npx tsx packages/engine/src/cli.ts daemon installnpx tsx packages/engine/src/cli.ts daemon statusnpx tsx packages/engine/src/cli.ts daemon uninstallinstall writes a LaunchAgent plist configured to run at load and to keep the process
alive, logging to <home>/runs/daemon.log, then bootstraps and kickstarts the job.

daemon status reports whether launchd currently owns the engine.
What the plist contains
Section titled “What the plist contains”~/Library/LaunchAgents/com.latchai.engine.plist, in four load-bearing keys:
| Key | Value |
|---|---|
Label |
com.latchai.engine |
ProgramArguments |
your node binary, the tsx CLI, packages/engine/src/cli.ts, serve |
RunAtLoad / KeepAlive |
both true — start at login, restart on crash |
StandardOutPath / StandardErrorPath |
<root>/runs/daemon.log |
There are no environment variables in it, so a launchd-started engine resolves its home
the usual way: LATCHAI_HOME (unset here) → the ~/.config/latchai/home pointer → ~/LatchAI.
Checking on it
Section titled “Checking on it”npx tsx packages/engine/src/cli.ts daemon status# [latchai] com.latchai.engine: state=running pid=41207
curl -s localhost:7777/api/status | head -c 200 # the engine actually answeringtail -f ~/LatchAI/runs/daemon.log # everything it printsdaemon.log is where the engine’s console goes once nothing is attached to a terminal —
the boot line with the resolved provider, MCP connection statuses, secret-resolution
failures, and any crash stack that made KeepAlive restart it.
Two launchd quirks, already handled
Section titled “Two launchd quirks, already handled”Bootstrap can leave the job pended. Loading a job does not reliably start it, so the
installer issues an explicit kickstart rather than assuming. (It also waits for a previous
bootout to finish before bootstrapping — that teardown is asynchronous, and a bootstrap
issued too soon fails with an unhelpful I/O error.)
launchd’s PATH is minimal. That breaks spawning npx-based
MCP servers, which is exactly the sort of failure that only shows up
overnight. LatchAI prepends node’s own bin directory and Homebrew (/opt/homebrew/bin,
/usr/local/bin) when spawning MCP servers so a launchd-started daemon has the same tool
surface a terminal-started one does.
Living with a background daemon
Section titled “Living with a background daemon”The engine has no hot reload. The daemon loads engine code into memory at startup, so
edits to packages/engine — and to mounts.json, which is read once at boot — require a
restart to take effect. A daemon left running across code edits silently executes stale
logic. It does re-read agents and workflows from disk per run, and re-arms triggers when
the workflows directory changes, so day-to-day authoring needs no restart.
The restart recipe, under launchd:
launchctl kickstart -k gui/$(id -u)/com.latchai.engine-k kills the running instance first, so this is the “I just upgraded the engine” button.
Without launchd, stop the terminal process and start it again — same effect, more typing.
When you launch the desktop app while a daemon already owns :7777, the app attaches
to that process rather than starting its own, and says so only in its console. The window
still shows your latest UI (the engine serves the bundle from disk), so the symptom is
narrow and confusing: UI changes apply, engine changes don’t. The tray menu’s Restart
Engine stops whatever owns the port — external daemon included — starts a fresh engine,
and reloads the window.
To find out which process is actually serving:
lsof -ti tcp:7777 # the pidps -o command= -p <pid> # …and what it isA launchd-owned engine shows up with the tsx CLI in its command line and, because
KeepAlive is on, restarts if you kill it directly — bootout or daemon uninstall first
if you want it gone.
What this doesn’t cover
Section titled “What this doesn’t cover”The launchd integration is macOS-specific. Running the engine as a service on Linux — a systemd unit around the same command — is not documented here yet.