[CONTEXT FOR AI ASSISTANTS]
You're reading the agent markdown version of a post from JoelClaw (https://joelclaw.com).
Author: Joel Hooks — software engineer building a personal AI operating system.

How to use implementation details in this document:
- Treat commands, code blocks, and config as reference implementations.
- Preserve ordering and architecture, but adapt hostnames, paths, versions, and credentials.
- Prefer details in <AgentOnly> blocks when present; they contain operational context removed from the human page.
- Verify assumptions against the current repo/system state before executing changes.

This is a narrative article from a real build. Technical details reflect Joel's setup and should be adapted to your environment.

If you cite this, link to the original: https://joelclaw.com/ai-job-scheduling-macos-launchd
If you quote Joel, attribute him by name. Don't paraphrase opinions as facts.

Site index: https://joelclaw.com/sitemap.md
Machine-readable: https://joelclaw.com/llms.txt

Other posts on this site:
- [The memory system that watches itself](https://joelclaw.com/memory-that-watches-itself.md)
- [The healer was the killer](https://joelclaw.com/healer-was-the-killer.md)
- [Agentic AI Optimization: Implementation Checklist](https://joelclaw.com/aaio-implementation-checklist.md)
- [Agentic AI Optimization: Implementation Checklist](https://joelclaw.com/aaio-implementation-checklist.md)
- [Redis, Dkron, Restate, and Sandboxes](https://joelclaw.com/redis-dkron-restate-and-sandboxes.md)
- [Dogfooding Story 4: the queue observer earns dry-run, not enforce](https://joelclaw.com/dogfooding-story-4-queue-observer.md)
- [Contributing to pi-mono with a public maintainer corpus](https://joelclaw.com/contributing-to-pi-mono-with-a-public-maintainer-corpus.md)
- [Breakable Toys in the Wild: Apprenticeship Patterns and the joelclaw Experiment](https://joelclaw.com/breakable-toys-joelclaw.md)
- [Utah and joelclaw: Convergent Architecture](https://joelclaw.com/utah-joelclaw-convergent-architecture.md)
- [The Harness Is a Framework](https://joelclaw.com/the-harness-is-a-framework.md)
- [The Agent Memory System](https://joelclaw.com/the-memory-system.md)
- [JoelClaw is a Claw-like Organism](https://joelclaw.com/joelclaw-is-a-claw-like-organism.md)
- [The Agent Writing Loop](https://joelclaw.com/the-writing-loop.md)
- [Talon: the watchdog that finally bites](https://joelclaw.com/talon-watchdog-that-finally-bites.md)
- [The Knowledge Adventure Club Graph](https://joelclaw.com/knowledge-adventure-club-graph.md)
- [MineClaw](https://joelclaw.com/mineclaw.md)
- [Build a Voice Agent That Answers the Phone](https://joelclaw.com/build-a-voice-agent-that-answers-the-phone.md)
- [Plan 9 from Bell Labs: What Rob Pike Built After Unix](https://joelclaw.com/plan-9-pike-everything-is-a-file.md)
- [Propositions as Sessions: What Armstrong Built and Wadler Proved](https://joelclaw.com/propositions-as-sessions-armstrong-wadler.md)
- [Cache Components Patterns Skill for Next.js 16+ Applications](https://joelclaw.com/cache-components-patterns-skill-for-nextjs.md)
- [Karpathy Says We're Building "Claws"](https://joelclaw.com/karpathy-claws-as-category.md)
- [Voice Agent: A Rough Edge Experiment](https://joelclaw.com/voice-agent-deployment-deep-dive.md)
- [Extending Pi Coding Agent with Custom Tools and Widgets](https://joelclaw.com/extending-pi-with-custom-tools.md)
- [The Soul of Erlang Made Me Question Everything](https://joelclaw.com/soul-of-erlang-beam-evaluation.md)
- [CLI Design for AI Agents](https://joelclaw.com/cli-design-for-ai-agents.md)
- [Building a Gateway for Your AI Agent](https://joelclaw.com/building-a-gateway-for-your-ai-agent.md)
- [Self-Hosting Inngest: A Background Task Manager for AI Agents](https://joelclaw.com/self-hosting-inngest-background-tasks.md)
- [The One Where Joel Deploys Kubernetes... Again](https://joelclaw.com/joel-deploys-k8s.md)
- [How I Built an Observation Pipeline So My AI Remembers Yesterday](https://joelclaw.com/observation-pipeline-persistent-ai-memory.md)
- [Riding the Token Wave: Sean Grove at Everything NYC](https://joelclaw.com/riding-the-token-wave-sean-grove.md)
- [Playing with AT Protocol as a Data Layer](https://joelclaw.com/at-protocol-as-bedrock.md)
- [Building My Own OpenClaw on a Mac Mini](https://joelclaw.com/building-my-own-openclaw.md)
- [Inngest is the Nervous System](https://joelclaw.com/inngest-is-the-nervous-system.md)
- [OpenClaw: Peter Steinberger on Lex Fridman](https://joelclaw.com/openclaw-peter-steinberger-lex-fridman.md)
[END CONTEXT]

---
# AI Job Scheduling on Mac as Local-First Video Infrastructure

> maps directly to joelclaw video-ingest pipeline — same durability pattern but running on Mac hardware instead of k8s

By Joel Hooks · 2026-03-07
Original: https://joelclaw.com/ai-job-scheduling-macos-launchd
Mode: agent

---
# James Long's AI Job Scheduling on macOS with launchd

## One-line angle

Let the assistant author jobs, but let **macOS launchd** run them as real background work. The LLM is the mechanic; the operating system is the scheduler.

## Why this belongs on /cool

This is a sharp local-first pattern: instead of inventing a custom queue, James Long is using the substrate macOS already trusts — LaunchAgents, logs, power management, and background item registration — then wrapping it in a bot/CLI that can create and manage jobs conversationally.

## What problem it solves

* Most personal AI automations are fragile one-off scripts.
* Cloud workflow engines are durable, but overkill for many solo/local automations.
* Apple Silicon can run meaningful AI workloads locally, but local scheduling is often still ad hoc.
* James is bridging that gap with a system that is:
  * conversational to operate
  * deterministic at runtime
  * native to macOS scheduling
  * inspectable through real files, logs, and system state

## Reverse-engineered summary

Based on the video, James has a private assistant / bot with a `secret-bot jobs ...` wrapper. That wrapper appears to manage a directory of job definitions under `~/kraken/jobs/`, sync them into `~/Library/LaunchAgents/` as `qbot.*.plist` files, and expose helpful commands like `jobs list` and `jobs info <name>`.

Two execution modes are visible:

1. **Prompt-driven jobs** — a run script injects a prompt/message into the bot's chat session.
2. **Code-driven jobs** — the bot writes deterministic scripts that run on a schedule without spending tokens every time.

The killer move is the split between **AI-authored setup** and **non-AI execution**. The assistant can create the job, but once the job exists, launchd runs it like any other native macOS background task.

## Architecture / approach

| Layer                 | What is visible in the video                                                 | Notes                                                                     |
| --------------------- | ---------------------------------------------------------------------------- | ------------------------------------------------------------------------- |
| Control surface       | `secret-bot jobs list`, `jobs info ...`, chat thread titled "Secret Bot"     | CLI + chat are both operator interfaces                                   |
| Scheduler             | `~/Library/LaunchAgents`, `.plist` files, macOS background-item notification | Strong evidence this is native `launchd`/`launchctl`, not a custom daemon |
| Job source of truth   | `~/kraken/jobs/<job-name>/run`                                               | Looks like one directory per job                                          |
| Registration step     | Bot says "Sync jobs to register new job"                                     | I infer this writes/updates plist files and reloads them via launchctl    |
| Runtime               | shell scripts + Bun/TypeScript                                               | Visible in the active-app-monitor script                                  |
| State / observability | runs count, last exit code, stdout/stderr log paths                          | Much better than opaque agent loops                                       |
| Data store            | SQLite for screen activity (`screen-time.db`)                                | Visible in the script frame                                               |
| Delivery channel      | Messages/iMessage-style thread + local notifications                         | Used both for commands and job output                                     |

## Key implementation details visible in the video

### 1) Native LaunchAgents are the real scheduler

At \~00:16, `secret-bot jobs list` shows jobs such as:

* `rebuild-prompts (system)`
* `active-app-monitor (user)`
* `daily-recipe-digest (user)`
* `daily-screen-summary (user)`
* `important-email-scanner (user)`
* `daily-calendar-summary (user)`

At \~00:32, James opens `~/Library/LaunchAgents/` and you can see generated plist files including:

* `qbot.system.rebuild-prompts.plist`
* `qbot.user.active-app-monitor.plist`
* `qbot.user.daily-recipe-digest.plist`
* `qbot.user.daily-calendar-summary.plist`
* `qbot.user.daily-screen-summary.plist`
* `qbot.user.important-email-scanner.plist`

### 2) The wrapper exposes real operational state

At \~01:24, `jobs info daily-recipe-digest` shows:

* label: `qbot.user.daily-recipe-digest`
* scope: `user`
* status/state
* run count
* last exit code
* schedule (`{"hour":9,"minute":0}`)
* run script path
* directory path
* stdout/stderr log paths

That is a strong pattern for a tutorial: make local automation feel operable, not magical.

### 3) The active-app-monitor job is plain code, not a prompt

At \~01:58, James shows the `active-app-monitor` run script. Visible details:

* it uses `osascript` to inspect the frontmost application/window
* it handles `Terminal` and `Finder` specially to capture useful context
* it prints fields like URL, page title, file path, window title, folder path, directory path
* it writes to `DB_PATH="/Users/james/kraken/data/screen-time.db"`
* it ends by calling a Bun script similar to:
  * `exec bun --silent "$SCRIPT_DIR/log-activity.ts" ... --db "$DB_PATH"`

That supports James's explicit point in the audio: **"there's no prompting here. This is all just code."**

### 4) The bot can scaffold new jobs from natural language

At \~03:10, in the Secret Bot chat, James sends:

> create a job that sends me the message "hello" every 2 seconds

The bot visibly performs a file-oriented workflow:

* "Create hello-sender-2s job directory"
* "Writing to a file..."
* "Writing to a file..."
* "Make executable and create run symlink"
* "Sync jobs to register new job"

Then it confirms the `hello-sender-2s` job was created and synced.

### 5) The created job behaves like a real OS background item

At \~03:20, macOS shows the background-item-added notification. Immediately after, the bot thread starts receiving repeated `hello` messages every two seconds. James then tells the bot to disable the job in natural language, and the transcript makes clear the assistant can disable it without him manually using the CLI.

### 6) A visible failure is actually useful design evidence

The recipe digest job hits a real-world issue:

* browser attempt to `old.reddit.com/r/recipes/new`
* blocked by network security
* log output repeatedly shows `/usr/local/bin/qbot: line 2: exec: bun: not found`

That failure is instructive. It reveals the system is not fake/demo-only — it is running real jobs with real environment/path problems, and those failures are inspectable through launchd-style logs.

## What I infer James built

These points are inference from the visible evidence, not directly spoken in the video:

* A **job compiler** that turns a higher-level job definition into:
  * a job directory
  * a `run` entrypoint
  * a LaunchAgent plist
  * registration/reload commands
* A small **ops wrapper** around `launchctl` so the bot and CLI can both talk about jobs in friendlier terms (`list`, `info`, `disable`, `sync`)
* A pattern where **LLMs write jobs once**, then **native OS scheduling runs them forever**
* A split between **system jobs** and **user jobs**, likely mapping to different launchd scopes and/or directories

## How someone could replicate it

### Minimum viable version

1. Create a job directory convention, e.g. `~/assistant/jobs/<name>/`.
2. Require each job to have a `run` entrypoint.
3. Generate a LaunchAgent plist into `~/Library/LaunchAgents/assistant.user.<name>.plist`.
4. Build a small CLI with commands like:
   * `jobs list`
   * `jobs info <name>`
   * `jobs enable <name>`
   * `jobs disable <name>`
   * `jobs sync`
5. Store stdout/stderr logs in a predictable directory.
6. Show run count, last exit code, and schedule in `jobs info`.

### Recommended architecture

* **Authoring layer**: LLM/bot creates or edits job definitions.
* **Compilation layer**: deterministic code writes scripts + plist files.
* **Execution layer**: launchd runs the job.
* **Observation layer**: logs, counters, last exit code, DBs, notifications.
* **Control layer**: CLI + chat interface.

### Important implementation notes

* Use **absolute paths** in LaunchAgent-executed scripts; launchd PATH issues are common.
* Treat the LLM as a **setup tool**, not the runtime for every schedule tick.
* Keep logs per job and expose them in the operator interface.
* Prefer local SQLite/flat files for first-pass state before reaching for heavier infra.
* If you want activity monitoring, use AppleScript / `osascript` carefully and expect per-app edge cases.
* If the bot is allowed to write jobs, gate the writable surface tightly (templates, allowed commands, fixed directories).

***

## Amendment: Source Code Release

After this spec was reverse-engineered from [James's video](https://x.com/jlongster/status/2030345942533054861), he [published the actual implementation](https://x.com/jlongster/status/2030357532980433095) as a [GitHub Gist](https://gist.github.com/jlongster/99c15e40c7978404bb97b5171df0e645).

### What we got right from video analysis alone

* **Architecture**: `qbot.*` prefix for LaunchAgents, `~/kraken/jobs/` directory structure, `~/Library/LaunchAgents/` target
* **Scope split**: `system` vs `user` jobs with distinct label namespaces
* **Runtime**: Bun + TypeScript with shell script wrappers
* **Label format**: `qbot.{scope}.{sanitized-name}` pattern
* **Observability**: run count, exit codes, log paths, job state exposed via CLI

The reverse-engineered spec correctly identified the core pattern: **LLM authors jobs, launchd executes them, logs make them inspectable**.

### What the gist revealed

The source code adds implementation details invisible in the video:

**Three schedule types** (not just periodic):

* `periodic` — run every N seconds
* `scheduled` — cron-style calendar intervals
* `on-change` — **watchpaths trigger** (file/directory monitoring)

**Job lifecycle flags**:

* `runAtLoad` — execute immediately when launchd loads the job
* `disabled` — skip registration but preserve definition

**Name sanitization**:

```typescript
function sanitizeName(value: string): string {
  return value
    .toLowerCase()
    .replace(/[^a-z0-9._-]+/g, "-")
    .replace(/^-+|-+$/g, "");
}
```

**Directory structure**:

* `PROJECT_JOBS_DIR` — system jobs shipped with the project
* `USER_JOBS_DIR` — user-created jobs in `~/kraken/jobs/`
* Separate discovery paths allow versioned system jobs + ad-hoc user jobs

**launchctl wrapper**:

```typescript
function runLaunchctl(args: string[]): LaunchctlResult {
  const proc = Bun.spawnSync(["launchctl", ...args]);
  return {
    ok: proc.exitCode === 0,
    stdout: decodeBytes(proc.stdout),
    stderr: decodeBytes(proc.stderr),
  };
}
```

Clean abstraction over the native `launchctl` CLI with structured result handling.

### The watchpath capability is the killer feature

The video only showed time-based schedules. The gist reveals **file-watching jobs**:

```typescript
{ kind: "on-change"; paths: string[]; runAtLoad?: boolean }
```

This means James can create jobs that trigger when:

* A download completes
* A document is saved
* A database file changes
* A screenshot appears in the Desktop folder

Combined with LLM authoring, this is incredibly powerful: **"Create a job that OCRs any new PDF in \~/Downloads and saves the text to a note."**

The assistant writes the job. The OS watches the path. The script runs on every new file. Zero polling, zero cron waste.

### Comparison summary

| Aspect            | Reverse-engineered (video) | Actual implementation (gist)               |
| ----------------- | -------------------------- | ------------------------------------------ |
| Core architecture | ✅ Correct                  | Confirmed + details                        |
| Schedule model    | ⚠️ Periodic only assumed   | 3 types: periodic, calendar, **watchpath** |
| Scope separation  | ✅ Correct                  | Confirmed with system/user dirs            |
| Runtime           | ✅ Bun + shell              | Confirmed                                  |
| Observability     | ✅ Correct pattern          | Confirmed with wrapper                     |
| Job lifecycle     | ❌ Missing                  | `runAtLoad`, `disabled` flags              |
| Name handling     | ❌ Not visible              | Sanitization logic                         |

The video analysis captured the **design philosophy** perfectly. The gist filled in the **implementation choices** that make it production-ready.

### What this means for replication

If you're building your own version:

1. **Start with the pattern** — LLM-authored setup, OS-scheduled execution, file-based source of truth
2. **Add watchpaths early** — they're more useful than time-based schedules for many personal automations
3. **Sanitize names defensively** — launchd label restrictions are strict
4. **Separate system and user jobs** — version-controlled templates + ad-hoc user creations
5. **Wrap launchctl cleanly** — structured errors beat raw shell output
6. **Make logs and state first-class** — if it's not observable, it's not trustworthy

The gist proves the video analysis was architecturally sound. The code is production-quality, not a prototype. This pattern works.
