# How to Configure LifeOS Background Jobs Using launchd Service on macOS

> Learn how to configure LifeOS background jobs on macOS using launchd services. This guide explains the plist templates, DeployComponents pipeline, and Services.ts wrapper for seamless setup.

- Repository: [Daniel Miessler 🛡️/LifeOS](https://github.com/danielmiessler/LifeOS)
- Tags: how-to-guide
- Published: 2026-08-12

---

**LifeOS configures macOS background jobs as `launchd` agents using plist templates, the `DeployComponents` pipeline, and a [`Services.ts`](https://github.com/danielmiessler/LifeOS/blob/main/Services.ts) wrapper that handles installation, templating, and agent bootstrapping.**

LifeOS provides deterministic background automation for macOS through native **launchd agents**. These jobs handle tasks like work sweeps, data synchronization, and system pulse monitoring. The configuration system relies on template-driven plist generation, environment-aware deployment scripts, and idempotent installation workflows that integrate with the broader LifeOS setup process.

## How launchd Configuration Works in LifeOS

### The Three-Layer Architecture

LifeOS implements launchd configuration through a coordinated pipeline:

- **Enhancement flags** in [`settings.enhancements.json`](https://github.com/danielmiessler/LifeOS/blob/main/settings.enhancements.json) control which jobs are enabled
- **[`DeployComponents.ts`](https://github.com/danielmiessler/LifeOS/blob/main/DeployComponents.ts)** orchestrates the deployment workflow
- **[`Services.ts`](https://github.com/danielmiessler/LifeOS/blob/main/Services.ts)** handles the low-level plist generation and `launchctl` operations

This separation keeps user configuration simple while ensuring robust, repeatable installations.

### Platform Detection

All launchd components are **macOS-only**. The codebase uses `DetectEnv.display`—a helper that detects GUI macOS environments—to conditionally skip launchd installation on Linux and Windows. This keeps the Setup workflow idempotent across platforms without manual intervention.

## Enabling Background Jobs in settings.enhancements.json

Individual launchd agents are toggled through boolean flags in the enhancements configuration file.

```json
// LifeOS/install/settings.enhancements.json
{
  "pulse": true,
  "worksweep": true,
  "derivedsync": false,
  "usageAggregator": true,
  "healthsync": false
}

```

Each key corresponds to a specific background job:

- **`pulse`** – Dashboard and system heartbeat service
- **`worksweep`** – Hourly work environment cleanup and synchronization
- **`derivedsync`** – Derived data synchronization across contexts
- **`usageAggregator`** – System usage metrics collection
- **`healthsync`** – Health data aggregation and sync

When the Setup workflow runs, it reads these flags and constructs a deployment plan containing only enabled components.

## The Deployment Pipeline

### Step 1: Setup Workflow Invocation

The top-level orchestrator is the **Setup workflow** located at [`LifeOS/install/Workflows/Setup.md`](https://github.com/danielmiessler/LifeOS/blob/main/LifeOS/install/Workflows/Setup.md). Run it via:

```bash

# From the repository root

bun run LifeOS/install/Workflows/Setup.md

```

### Step 2: DeployComponents Delegation

Inside [`DeployComponents.ts`](https://github.com/danielmiessler/LifeOS/blob/main/DeployComponents.ts) (lines 319-321), the pipeline iterates over selected components. For launchd services, it delegates to [`Services.ts`](https://github.com/danielmiessler/LifeOS/blob/main/Services.ts) according to the source code comment describing launchd handling:

```typescript
// LifeOS/install/LIFEOS/TOOLS/Services.ts lines 6-13
// Handles launchd agent installation:
// 1. Reads template from LifeOS/install/LIFEOS/TOOLS/*.plist.template
// 2. Substitutes placeholders ({{HOME}}, {{BUN_PATH}})
// 3. Writes to ~/Library/LaunchAgents/com.lifeos.<label>.plist
// 4. Executes launchctl bootstrap to load into user domain

```

### Step 3: Plist Generation and Installation

The `installLaunchd()` function performs four operations:

1. **Template loading** – Reads the component-specific `.plist.template` file
2. **Placeholder substitution** – Replaces `{{HOME}}` and `{{BUN_PATH}}` with resolved paths
3. **Plist deployment** – Writes to `~/Library/LaunchAgents/com.lifeos.<label>.plist`
4. **Agent bootstrap** – Executes `launchctl bootstrap "gui/$(id -u)" <plist>` to activate

The operation is **idempotent**—subsequent runs replace existing agents without error.

## Plist Template Structure

Each background job ships with a template defining schedule, execution policy, and logging. Here is the worksweep template:

```xml
<!-- com.lifeos.worksweep.plist.template -->
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
  "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
  <key>Label</key><string>com.lifeos.worksweep</string>
  <key>ProgramArguments</key>
  <array>
    <string>{{HOME}}/.claude/LIFEOS/bin/bun</string>
    <string>run</string>
    <string>LifeOS/install/LIFEOS/TOOLS/worksweep.ts</string>
  </array>
  <key>RunAtLoad</key><true/>
  <key>StartInterval</key><integer>3600</integer>   <!-- every hour -->
  <key>StandardOutPath</key><string>{{HOME}}/.claude/LIFEOS/MEMORY/OBSERVABILITY/worksweep.log</string>
  <key>StandardErrorPath</key><string>{{HOME}}/.claude/LIFEOS/MEMORY/OBSERVABILITY/worksweep.log</string>
</dict>
</plist>

```

Key launchd directives used across LifeOS templates:

| Directive | Purpose | Typical Value |
|-----------|---------|---------------|
| `RunAtLoad` | Execute immediately when agent loads | `true` |
| `StartInterval` | Seconds between executions | 3600 (1 hour), 300 (5 min) |
| `KeepAlive` | Restart if process exits | `false` (most jobs) |
| `StandardOutPath` / `StandardErrorPath` | Log file locations | `~/.claude/LIFEOS/MEMORY/OBSERVABILITY/` |

## Installing a Specific Job Manually

For programmatic control or custom workflows, import `installLaunchd` directly:

```typescript
import { installLaunchd } from '../LifeOS/install/LIFEOS/TOOLS/Services';

// Install the "worksweep" background job
await installLaunchd('worksweep');

// Install Pulse dashboard service
await installLaunchd('pulse');

```

This bypasses the full Setup workflow while using the same underlying machinery.

## Verifying launchd Agent Status

After installation, confirm proper loading with `launchctl`:

```bash

# General status check

launchctl print "gui/$(id -u)/com.lifeos.worksweep"

# Expected output includes:

#   PID = <number>

#   ProgramArguments = { "/Users/.../.claude/LIFEOS/bin/bun", "run", "..." }

#   State = running

#   Scheduled = <interval description>

```

The Setup workflow performs automatic verification at line 54, running `launchctl print` for each enabled label and surfacing errors before completion.

## Generated Plist Example

After template processing, the resolved plist for worksweep resembles:

```xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
    "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
  <key>Label</key><string>com.lifeos.worksweep</string>
  <key>ProgramArguments</key>
  <array>
    <string>/Users/daniel/.claude/LIFEOS/bin/bun</string>
    <string>run</string>
    <string>LifeOS/install/LIFEOS/TOOLS/worksweep.ts</string>
  </array>
  <key>RunAtLoad</key><true/>
  <key>StartInterval</key><integer>3600</integer>
  <key>StandardOutPath</key><string>/Users/daniel/.claude/LIFEOS/MEMORY/OBSERVABILITY/worksweep.log</string>
  <key>StandardErrorPath</key><string>/Users/daniel/.claude/LIFEOS/MEMORY/OBSERVABILITY/worksweep.log</string>
</dict>
</plist>

```

Bootstrapped agents appear in `~/Library/LaunchAgents/` and persist across reboots until explicitly unloaded.

## Summary

- **Configuration source**: [`settings.enhancements.json`](https://github.com/danielmiessler/LifeOS/blob/main/settings.enhancements.json) contains boolean flags for each launchd job
- **Deployment entry point**: [`DeployComponents.ts`](https://github.com/danielmiessler/LifeOS/blob/main/DeployComponents.ts) delegates to [`Services.ts`](https://github.com/danielmiessler/LifeOS/blob/main/Services.ts) for launchd-specific operations
- **Template processing**: [`Services.ts`](https://github.com/danielmiessler/LifeOS/blob/main/Services.ts) substitutes placeholders and writes plists to `~/Library/LaunchAgents/`
- **Activation**: `launchctl bootstrap "gui/$(id -u)" <plist>` loads agents into the user domain
- **Verification**: `launchctl print "gui/$(id -u)/com.lifeos.<label>"` confirms proper operation
- **Platform guard**: `DetectEnv.display` ensures launchd jobs install only on macOS with GUI environments

## Frequently Asked Questions

### Where are LifeOS launchd plists stored after installation?

LifeOS writes generated plist files to `~/Library/LaunchAgents/` following Apple's standard for user-specific launchd agents. Each file follows the naming convention `com.lifeos.<component>.plist`, such as `com.lifeos.worksweep.plist` or `com.lifeos.pulse.plist`.

### Can I modify the schedule of a LifeOS background job?

Yes. Edit the corresponding `.plist.template` file in `LifeOS/install/LIFEOS/TOOLS/`, then rerun the Setup workflow. Modify the `StartInterval` integer (seconds) or replace it with `StartCalendarInterval` for calendar-based scheduling. Changes take effect after `launchctl bootstrap` reloads the agent.

### What happens if I run Setup multiple times?

The installation is **idempotent**. `Services.installLaunchd()` replaces existing plists and re-bootstraps agents without error. The `launchctl bootstrap` command handles replacement gracefully, and log files append rather than truncate.

### How do I temporarily disable a LifeOS launchd job without uninstalling?

Use `launchctl` directly to unload the agent while preserving the plist:

```bash
launchctl bootout "gui/$(id -u)/com.lifeos.worksweep"

```

To re-enable later:

```bash
launchctl bootstrap "gui/$(id -u)" ~/Library/LaunchAgents/com.lifeos.worksweep.plist

```

Alternatively, set the corresponding flag to `false` in [`settings.enhancements.json`](https://github.com/danielmiessler/LifeOS/blob/main/settings.enhancements.json) and rerun Setup to remove the agent entirely.