You're going to get a job running on launchd before you understand how any of it works. Empty directory to working scheduler in a few minutes.
Init the Project
mkdir launchd-ai-scheduler && cd launchd-ai-scheduler
bun init -y
Install Dependencies
bun add -d @types/bun typescript
Add to your tsconfig.json:
{
"compilerOptions": {
"target": "ESNext",
"module": "ESNext",
"moduleResolution": "bundler",
"strict": true,
"types": ["@types/bun"]
}
}
Create Your First Job
A job is a directory with two files — a schedule and a run script:
mkdir -p system-jobs/hello-world
The schedule — run every 60 seconds:
echo '60' > system-jobs/hello-world/schedule
The run script — log a timestamp:
cat > system-jobs/hello-world/run << 'EOF'
#!/bin/bash
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Hello from launchd! Job: ${JOB_LABEL:-unknown}"
EOF
chmod 755 system-jobs/hello-world/run
That's a complete job definition. A directory, a schedule file, an executable script.
Build the Manager
Create src/manager.ts — this is the core. It discovers job directories, generates launchd plists, and registers them. Your companion can help you build this step by step, or you can write it yourself. The key functions:
parseScheduleText(raw)— reads the schedule fileplistForJob(job)— generates launchd XMLinstallJob(job)— writes the plist, registers with launchctlsyncAllJobs()— discovers all jobs, installs enabled ones, removes stale ones
Ask your companion: "teach me how to build the manager"
Build the Sync Entrypoint
Create src/sync.ts:
import { syncAllJobs } from "./manager";
const report = await syncAllJobs();
console.log(`[jobs] Synced ${report.synced.length} jobs`);
if (report.synced.length > 0) console.log(`[jobs] Updated: ${report.synced.join(", ")}`);
if (report.removed.length > 0) console.log(`[jobs] Removed: ${report.removed.join(", ")}`);
Add to package.json:
{
"scripts": {
"sync": "bun run src/sync.ts"
}
}
Run It
bun run sync
[jobs] Synced 1 jobs
[jobs] Updated: aijs.system.hello-world
Verify
launchctl list | grep aijs
- 0 aijs.system.hello-world
Your job is registered with launchd. It will run every 60 seconds. It survives reboots. The OS manages it.
Kick It
Force-run now instead of waiting:
launchctl kickstart gui/$(id -u)/aijs.system.hello-world
Check the output:
cat workdir/logs/hello-world/stdout.log
[2026-03-13 17:57:42] Hello from launchd! Job: aijs.system.hello-world
It works. You built a job scheduler. The rest of this course is understanding how each piece works and making it do real AI work.