local-automation
Creates persistent, recurring local automations on macOS using launchd. Interviews for requirements, creates wrapper scripts and plists, loads the job, tests it, and updates the project context map.
SKILL.mdskills/local-automation/SKILL.md
GitHub/local-automation — Create a Local Recurring Automation
Creates a persistent macOS launchd job that runs on a schedule, survives reboots, and logs output.
Phase 1: Interview
Ask these questions one at a time, waiting for the user's response before proceeding:
- What should this automation do? (plain English description)
- Does a script already exist, or do we need to create one? If it exists, ask for the path.
- What schedule? Offer common patterns:
- Daily at a specific time (e.g., "7 AM")
- Every N hours (e.g., "every 2 hours")
- Weekdays only at a specific time
- Custom (let them describe it)
- Does it need secrets from Vault? If yes, which labels? (These get loaded via
get-secret.sh) - Does it need to access
$OUTWORKOS_PARENT? (If yes, warn about Full Disk Access requirement for/bin/bash) - What should it be named? Suggest a kebab-case name based on the description (e.g.,
inbox-pulse,daily-backup). The name determines:- Plist:
~/Library/LaunchAgents/com.outworkos.{name}.plist - Script:
~/.local/bin/outworkos-{name}.sh - Log:
/tmp/outworkos-{name}.log - Label:
com.outworkos.{name}
- Plist:
After all questions are answered, summarize the plan and ask for confirmation before proceeding.
Phase 2: Analyze
Before creating anything, check for conflicts:
# Check if plist already exists
ls ~/Library/LaunchAgents/com.outworkos.{name}.plist 2>/dev/null
# Check if wrapper script already exists
ls ~/.local/bin/outworkos-{name}.sh 2>/dev/null
# Check if label is already loaded
launchctl list | grep com.outworkos.{name}
If any exist, ask the user whether to overwrite or choose a different name.
Review the source script (if existing) for:
- Dependencies (Python packages, CLI tools)
- Environment variable requirements
- File path assumptions (especially hardcoded paths)
- Whether it needs modification to work under launchd's minimal environment
Present findings and proposed changes before proceeding.
Phase 3: Develop
Step 1: Create the wrapper script
Write ~/.local/bin/outworkos-{name}.sh:
#!/bin/bash
# outworkos-{name}.sh — {description}
# Managed by /local-automation. Do not edit directly.
set -euo pipefail
# --- Vault secrets (if needed) ---
GET_SECRET="${OUTWORKOS_ROOT}/scripts/get-secret.sh"
# export SECRET_NAME=$("$GET_SECRET" secret_label)
# --- Run the automation ---
# Either inline logic or call the source script
Key rules for the wrapper:
- Always use
set -euo pipefail - Load secrets via
$GET_SECRETif needed - Use absolute paths everywhere (launchd has no working directory context)
- Keep it minimal — delegate to the actual script/command
chmod +x ~/.local/bin/outworkos-{name}.sh
Step 2: Create the launchd plist
Write ~/Library/LaunchAgents/com.outworkos.{name}.plist:
<?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.outworkos.{name}</string>
<key>ProgramArguments</key>
<array>
<string>$HOME/.local/bin/outworkos-{name}.sh</string>
</array>
<!-- SCHEDULE: use ONE of the following patterns -->
<!-- Daily at specific time: -->
<key>StartCalendarInterval</key>
<dict>
<key>Hour</key>
<integer>{hour}</integer>
<key>Minute</key>
<integer>{minute}</integer>
</dict>
<!-- Every N seconds: -->
<!-- <key>StartInterval</key> -->
<!-- <integer>{seconds}</integer> -->
<!-- Weekdays at specific time: -->
<!-- <key>StartCalendarInterval</key> -->
<!-- <array> -->
<!-- <dict><key>Weekday</key><integer>1</integer><key>Hour</key><integer>{h}</integer><key>Minute</key><integer>{m}</integer></dict> -->
<!-- ... repeat for days 2-5 ... -->
<!-- </array> -->
<key>StandardOutPath</key>
<string>/tmp/outworkos-{name}.log</string>
<key>StandardErrorPath</key>
<string>/tmp/outworkos-{name}.log</string>
<key>EnvironmentVariables</key>
<dict>
<key>PATH</key>
<string>/usr/local/bin:/usr/bin:/bin:/opt/homebrew/bin</string>
<key>HOME</key>
<string>{USER_HOME}</string>
</dict>
</dict>
</plist>
Schedule mapping:
- "Daily at 7 AM" →
StartCalendarIntervalwithHour=7, Minute=0 - "Every 2 hours" →
StartIntervalwith7200 - "Weekdays at 9 AM" →
StartCalendarIntervalarray withWeekday1-5 - "Every 30 minutes" →
StartIntervalwith1800
Only include ONE scheduling key. Remove the commented-out alternatives from the final plist.
Step 3: Load and test
# Load the job
launchctl load ~/Library/LaunchAgents/com.outworkos.{name}.plist
# Verify it's loaded (exit code 0 = loaded, status "-" = not yet run)
launchctl list | grep com.outworkos.{name}
# Run it once now to test
launchctl start com.outworkos.{name}
# Wait a few seconds, then check the log
sleep 3
cat /tmp/outworkos-{name}.log
If the test fails:
- Check the log for errors
- Common issues: missing PATH entries, Full Disk Access, missing deps
- Fix the wrapper script and re-test (no need to reload the plist for script changes)
- If plist changes are needed:
launchctl unloadthenlaunchctl loadagain
Step 4: Update context map
Find the project's context-map.md and add the automation under an ## Automations section (create the section if it doesn't exist):
## Automations
| Name | Schedule | Script | Plist | Log | Description |
|------|----------|--------|-------|-----|-------------|
| {name} | {human-readable schedule} | `~/.local/bin/outworkos-{name}.sh` | `~/Library/LaunchAgents/com.outworkos.{name}.plist` | `/tmp/outworkos-{name}.log` | {description} |
If the automation belongs to a specific project (not OutworkOS itself), update that project's context map instead.
Conventions
- All labels use
com.outworkos.prefix - All wrapper scripts live at
~/.local/bin/outworkos-{name}.sh - All plists live at
~/Library/LaunchAgents/com.outworkos.{name}.plist - All logs go to
/tmp/outworkos-{name}.log - Secrets are always loaded via
$OUTWORKOS_ROOT/scripts/get-secret.sh, never hardcoded - Wrapper scripts must use absolute paths and never assume a working directory
Troubleshooting Reference
| Symptom | Cause | Fix |
|---|---|---|
Operation not permitted in log |
Full Disk Access not granted to /bin/bash |
System Settings > Privacy & Security > Full Disk Access > add /bin/bash |
Exit code non-zero in launchctl list |
Script is crashing | Check /tmp/outworkos-{name}.log for errors |
Job doesn't appear in launchctl list |
Plist not loaded | launchctl load ~/Library/LaunchAgents/com.outworkos.{name}.plist |
| Job runs but script not found | Wrong path in plist | Verify ProgramArguments path matches actual script location |
| Secrets fail to load | Keychain locked or missing service_role_key |
Run security unlock-keychain or check Keychain Access |
| Python import errors | Deps not installed for system Python | python3 -m pip install {package} |