
Scheduler
- 548 installs
- 508 repo stars
- Updated January 19, 2026
- jshchnz/claude-code-scheduler
scheduler is a Claude skill that generates, validates, and explains five-field cron expressions for developers scheduling recurring agent tasks, jobs, or automations.
About
scheduler is a Claude Code skill with a complete cron expression reference for recurring job scheduling. It documents the standard five-field format—minute (0–59), hour (0–23), day of month (1–31), month (1–12), and day of week (0–6, Sunday=0)—plus special characters asterisk, comma lists, ranges, and step values. Examples include 0 9,17 * * * for 9:00 AM and 5:00 PM daily. Developers reach for scheduler when configuring Claude Code scheduled tasks, CI cron triggers, or any automation that needs a correct crontab string without misreading field order.
- Complete cron expression syntax reference with field ranges and special characters
- Detailed explanations of *, ,, -, / operators with real examples
- Common time-based patterns including every X minutes, hourly, and daily schedules
- Daily, weekly, and monthly scheduling patterns for production use
- Visual breakdown table for quick lookup during agent scheduling
Scheduler by the numbers
- 548 all-time installs (skills.sh)
- +2 installs in the week ending Aug 2, 2026 (Skillselion tracking)
- Ranked #402 of 2,715 Automation & Workflows skills by installs in the Skillselion catalog
- Security screen: MEDIUM risk (skills.sh audit)
- Data as of Aug 2, 2026 (Skillselion catalog sync)
npx skills add https://github.com/jshchnz/claude-code-scheduler --skill schedulerAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 548 |
|---|---|
| repo stars | ★ 508 |
| Security audit | 2 / 3 scanners passed |
| Last updated | January 19, 2026 |
| Repository | jshchnz/claude-code-scheduler ↗ |
How do you write a valid cron expression?
Generate, validate, and understand cron expressions for scheduling recurring agent tasks, jobs, or automations.
Who is it for?
Developers configuring recurring agent tasks, cron jobs, or scheduled automations who need correct five-field syntax.
Skip if: One-off delayed tasks better served by at-style schedulers or complex workflow orchestrators like Airflow DAGs.
When should I use this skill?
The user asks for a cron schedule, crontab syntax, recurring job timing, or help validating a cron expression.
What you get
Validated five-field cron strings with documented minute, hour, day, month, and weekday schedules.
- validated cron expressions
- cron syntax documentation
By the numbers
- Standard five-field cron covers minute 0–59, hour 0–23, day 1–31, month 1–12, weekday 0–6
- Example schedule 0 9,17 * * * defines two daily run times
Files
Scheduling Assistant
You help users set up and manage scheduled Claude Code tasks. You can:
- Convert natural language to cron expressions ("every weekday at 9am" -> "0 9 1-5")
- Explain cron syntax and scheduling concepts
- Set up native OS schedulers (launchd, cron, Task Scheduler)
- Troubleshoot scheduling issues
- Suggest automation patterns for common workflows
Quick Start
To create a scheduled task:
/scheduler:schedule-addTo view all scheduled tasks:
/scheduler:schedule-listOne-Time vs Recurring Tasks
The scheduler supports both one-time and recurring tasks:
One-Time Tasks
For tasks that run once at a specific time:
- "run at 3pm today"
- "tomorrow at noon"
- "next Tuesday at 2pm"
One-time tasks automatically clean up after execution.
Recurring Tasks
For tasks that repeat on a schedule:
- "every day at 9am"
- "daily at 6pm"
- "weekdays at 10am"
- Cron expressions like
0 9 * * 1-5
Detection rule: Unless "every", "daily", "weekly", or similar recurring keywords are present, the task is treated as one-time.
Git Worktree Mode (Isolated Branches)
For tasks that make changes, worktree mode runs them in isolation:
You: Every night at 2am, refactor deprecated API calls and push for review
Claude: Should this run in an isolated git worktree?
→ Yes, create branch and push changes
→ No, run in main working directory
You: Yes
Claude: ✓ Task created with worktree isolation
Branch prefix: claude-task/
Remote: originHow it works: 1. Task triggers → creates fresh worktree with new branch 2. Claude runs in the worktree (isolated from main) 3. Changes are committed and pushed to remote 4. Worktree is cleaned up after successful push 5. You review the PR at your convenience
Configuration options:
| Option | Default | Description |
|---|---|---|
worktree.enabled | false | Enable worktree isolation |
worktree.branchPrefix | "claude-task/" | Branch name prefix |
worktree.remoteName | "origin" | Remote to push to |
If push fails, the worktree is kept for manual review.
Cron Quick Reference
* * * * *
| | | | |
| | | | +-- Day of week (0-6, Sun=0)
| | | +---- Month (1-12)
| | +------ Day of month (1-31)
| +-------- Hour (0-23)
+---------- Minute (0-59)Common patterns:
| Pattern | Description |
|---|---|
0 9 * * * | Daily at 9:00 AM |
0 9 * * 1-5 | Weekdays at 9:00 AM |
*/15 * * * * | Every 15 minutes |
0 */2 * * * | Every 2 hours |
0 9 1 * * | First of month at 9:00 AM |
0 9 * * 1 | Every Monday at 9:00 AM |
For complete syntax, see CRON_REFERENCE.md.
Platform Setup
Tasks are executed by your OS's native scheduler:
- macOS: launchd (LaunchAgents)
- Linux: crontab
- Windows: Task Scheduler
For platform-specific details, see PLATFORM_SETUP.md.
Common Use Cases
Daily Code Review
Schedule: 0 9 * * 1-5 (weekdays at 9am)
Command: /review-code --scope=yesterdayWeekly Dependency Audit
Schedule: 0 10 * * 1 (Mondays at 10am)
Command: Check for outdated dependencies and security vulnerabilitiesAutomated Testing
Schedule: 0 */4 * * * (every 4 hours)
Command: Run test suite and report failuresTroubleshooting
Task not running? 1. Check /scheduler:schedule-status for health 2. Verify task is enabled: /scheduler:schedule-list 3. Check logs: /scheduler:schedule-logs <task-id> 4. Ensure claude CLI is in PATH for scheduler
Common issues:
- PATH not set correctly in scheduler environment
- Working directory doesn't exist
- Command syntax errors
- Scheduler daemon not running
Helper Scripts
To validate a cron expression:
python scripts/parse-cron.py "0 9 * * 1-5"Available Commands
| Command | Description |
|---|---|
/scheduler:schedule-add | Create a new scheduled task |
/scheduler:schedule-list | View all scheduled tasks |
/scheduler:schedule-remove <id> | Remove a scheduled task |
/scheduler:schedule-status | Check scheduler health |
/scheduler:schedule-run <id> | Manually run a task |
/scheduler:schedule-logs <id> | View execution logs |
Cron Expression Reference
Complete guide to cron expression syntax.
Basic Format
┌───────────── minute (0-59)
│ ┌───────────── hour (0-23)
│ │ ┌───────────── day of month (1-31)
│ │ │ ┌───────────── month (1-12)
│ │ │ │ ┌───────────── day of week (0-6, Sunday=0)
│ │ │ │ │
* * * * *Field Values
| Field | Range | Special Characters |
|---|---|---|
| Minute | 0-59 | * , - / |
| Hour | 0-23 | * , - / |
| Day of Month | 1-31 | * , - / |
| Month | 1-12 | * , - / |
| Day of Week | 0-6 (Sun=0) | * , - / |
Special Characters
Asterisk (*)
Matches all values.
* * * * *= Every minute
Comma (,)
List of values.
0 9,17 * * *= At 9:00 AM and 5:00 PM
Hyphen (-)
Range of values.
0 9-17 * * *= Every hour from 9 AM to 5 PM0 9 * * 1-5= Weekdays at 9:00 AM
Slash (/)
Step values.
*/15 * * * *= Every 15 minutes0 */2 * * *= Every 2 hours0 9-17/2 * * *= Every 2 hours from 9 AM to 5 PM
Common Patterns
Time-based
| Pattern | Description |
|---|---|
* * * * * | Every minute |
*/5 * * * * | Every 5 minutes |
*/15 * * * * | Every 15 minutes |
*/30 * * * * | Every 30 minutes |
0 * * * * | Every hour (at minute 0) |
0 */2 * * * | Every 2 hours |
0 */4 * * * | Every 4 hours |
0 */6 * * * | Every 6 hours |
0 */12 * * * | Every 12 hours |
Daily
| Pattern | Description |
|---|---|
0 0 * * * | Daily at midnight |
0 6 * * * | Daily at 6:00 AM |
0 9 * * * | Daily at 9:00 AM |
0 12 * * * | Daily at noon |
0 18 * * * | Daily at 6:00 PM |
0 23 * * * | Daily at 11:00 PM |
30 9 * * * | Daily at 9:30 AM |
Weekdays/Weekends
| Pattern | Description |
|---|---|
0 9 * * 1-5 | Weekdays at 9:00 AM |
0 9 * * 0,6 | Weekends at 9:00 AM |
0 9 * * 1 | Every Monday at 9:00 AM |
0 9 * * 5 | Every Friday at 9:00 AM |
0 17 * * 5 | Every Friday at 5:00 PM |
Monthly
| Pattern | Description |
|---|---|
0 9 1 * * | First day of month at 9:00 AM |
0 9 15 * * | 15th of month at 9:00 AM |
0 9 1,15 * * | 1st and 15th at 9:00 AM |
0 0 1 * * | First day of month at midnight |
Yearly
| Pattern | Description |
|---|---|
0 0 1 1 * | January 1st at midnight |
0 9 1 1 * | January 1st at 9:00 AM |
0 9 1 */3 * | First day of quarter at 9:00 AM |
Day of Week Values
| Value | Day |
|---|---|
| 0 | Sunday |
| 1 | Monday |
| 2 | Tuesday |
| 3 | Wednesday |
| 4 | Thursday |
| 5 | Friday |
| 6 | Saturday |
| 7 | Sunday (alternative) |
Month Values
| Value | Month |
|---|---|
| 1 | January |
| 2 | February |
| 3 | March |
| 4 | April |
| 5 | May |
| 6 | June |
| 7 | July |
| 8 | August |
| 9 | September |
| 10 | October |
| 11 | November |
| 12 | December |
Complex Examples
Business hours
0 9-17 * * 1-5Every hour from 9 AM to 5 PM on weekdays.
Twice daily on weekdays
0 9,17 * * 1-5At 9:00 AM and 5:00 PM, Monday through Friday.
Every 30 minutes during business hours
*/30 9-17 * * 1-5First Monday of each month
0 9 1-7 * 1At 9:00 AM on the first Monday (day 1-7 AND Monday).
Last day of month (approximate)
0 9 28-31 * *At 9:00 AM on days 28-31 (runs multiple times in long months).
Natural Language Conversion
| Natural Language | Cron Expression |
|---|---|
| "every minute" | * * * * * |
| "every hour" | 0 * * * * |
| "every day at 9am" | 0 9 * * * |
| "every weekday at 9am" | 0 9 * * 1-5 |
| "every Monday at 10am" | 0 10 * * 1 |
| "every 15 minutes" | */15 * * * * |
| "twice daily" | 0 9,17 * * * |
| "weekly" | 0 9 * * 1 |
| "monthly" | 0 9 1 * * |
Validation
Use the helper script to validate expressions:
python scripts/parse-cron.py "0 9 * * 1-5"Or use online tools:
Platform Notes
macOS (launchd)
- Uses
StartCalendarIntervalin plist - Some complex expressions may be simplified
- Step values have limited support
Linux (crontab)
- Full cron syntax support
- Entries added to user's crontab
Windows (Task Scheduler)
- Maps to schedule types (DAILY, WEEKLY, MONTHLY)
- Some cron features may not translate exactly
Platform-Specific Setup Guide
Detailed information about how scheduled tasks work on each platform.
macOS (launchd)
How It Works
On macOS, scheduled tasks are managed by launchd, Apple's service management framework. Tasks are registered as LaunchAgents using plist (property list) files.
File Locations
- Plist files:
~/Library/LaunchAgents/com.claude.scheduler.<task-id>.plist - Log files:
~/.claude/logs/<task-id>.out.logand<task-id>.err.log
Manual Commands
List loaded agents:
launchctl list | grep claude.schedulerLoad an agent:
launchctl load ~/Library/LaunchAgents/com.claude.scheduler.<task-id>.plistUnload an agent:
launchctl unload ~/Library/LaunchAgents/com.claude.scheduler.<task-id>.plistCheck agent status:
launchctl list com.claude.scheduler.<task-id>Plist Structure
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "...">
<plist version="1.0">
<dict>
<key>Label</key>
<string>com.claude.scheduler.task-id</string>
<key>ProgramArguments</key>
<array>
<string>/bin/bash</string>
<string>-c</string>
<string>cd "/path/to/project" && claude -p "command"</string>
</array>
<key>StartCalendarInterval</key>
<dict>
<key>Hour</key>
<integer>9</integer>
<key>Minute</key>
<integer>0</integer>
<key>Weekday</key>
<integer>1</integer>
</dict>
<key>StandardOutPath</key>
<string>/Users/you/.claude/logs/task-id.out.log</string>
<key>StandardErrorPath</key>
<string>/Users/you/.claude/logs/task-id.err.log</string>
<key>EnvironmentVariables</key>
<dict>
<key>PATH</key>
<string>/usr/local/bin:/usr/bin:/bin:/opt/homebrew/bin</string>
</dict>
</dict>
</plist>Troubleshooting
Task not running: 1. Check if agent is loaded: launchctl list | grep claude 2. Check for errors: cat ~/.claude/logs/<task-id>.err.log 3. Verify plist syntax: plutil -lint ~/Library/LaunchAgents/com.claude.scheduler.*.plist
PATH issues: launchd runs with minimal environment. Ensure PATH includes:
/usr/local/bin(Homebrew Intel)/opt/homebrew/bin(Homebrew Apple Silicon)- Location of
claudeCLI
---
Linux (crontab)
How It Works
On Linux, tasks are registered in the user's crontab. Each task is a single line in the crontab with a marker comment for identification.
File Locations
- Crontab: User's crontab (no direct file access)
- Log files:
~/.claude/logs/<task-id>.log
Manual Commands
View crontab:
crontab -lEdit crontab:
crontab -eRemove all entries:
crontab -rEntry Format
0 9 * * 1-5 cd "/path/to/project" && claude -p "/review-code" >> ~/.claude/logs/task-id.log 2>&1 # claude-scheduler:task-idComponents:
0 9 * * 1-5- Cron schedulecd "/path/to/project"- Change to working directoryclaude -p "/review-code"- Command to execute>> ~/.claude/logs/task-id.log 2>&1- Log output# claude-scheduler:task-id- Marker for identification
Troubleshooting
Task not running: 1. Check crontab: crontab -l | grep claude-scheduler 2. Check cron daemon: systemctl status cron or service cron status 3. Check system logs: grep CRON /var/log/syslog
PATH issues: Cron runs with minimal PATH. Options:
- Use absolute paths:
/usr/local/bin/claude - Set PATH in crontab: Add
PATH=/usr/local/bin:/usr/bin:/binat top - Use a wrapper script
Permission issues:
- Ensure execute permissions on any scripts
- Check file ownership matches crontab user
---
Windows (Task Scheduler)
How It Works
On Windows, tasks are registered with Task Scheduler using the schtasks.exe command-line tool. Tasks are organized in a \ClaudeScheduler folder.
File Locations
- Tasks: Task Scheduler > Task Scheduler Library > ClaudeScheduler
- Log files:
%USERPROFILE%\.claude\logs\<task-id>.log
Manual Commands
List tasks:
schtasks /Query /TN "\ClaudeScheduler" /FO LISTCreate task:
schtasks /Create /TN "\ClaudeScheduler\task-id" /TR "cmd /c claude -p \"command\"" /SC DAILY /ST 09:00Delete task:
schtasks /Delete /TN "\ClaudeScheduler\task-id" /FRun task immediately:
schtasks /Run /TN "\ClaudeScheduler\task-id"Schedule Types
| schtasks /SC | Description |
|---|---|
| MINUTE | Every N minutes |
| HOURLY | Every N hours |
| DAILY | Every N days |
| WEEKLY | Every N weeks |
| MONTHLY | Every N months |
| ONCE | One time only |
Troubleshooting
Task not running: 1. Open Task Scheduler GUI (taskschd.msc) 2. Navigate to ClaudeScheduler folder 3. Check task properties and history 4. Run as Administrator if needed
Permission issues:
- Some tasks require "Run with highest privileges"
- Check "Run whether user is logged on or not"
PATH issues:
- Use full path to claude.exe
- Or set PATH in the task's action
---
Common Issues (All Platforms)
Claude CLI Not Found
Symptom: Task fails with "claude: command not found"
Fix: 1. Find claude location: which claude (Unix) or where claude (Windows) 2. Ensure scheduler environment has correct PATH 3. Use absolute path in command
Working Directory Issues
Symptom: Task fails to find files or uses wrong directory
Fix: 1. Always specify absolute paths for working directory 2. Use cd command before main command 3. Verify directory exists
Timeout Issues
Symptom: Task killed before completion
Fix: 1. Increase timeout in task configuration 2. Check for infinite loops in command 3. Consider breaking into smaller tasks
Log File Issues
Symptom: No output in log files
Fix: 1. Ensure log directory exists: mkdir -p ~/.claude/logs 2. Check write permissions 3. Verify output redirection syntax
#!/usr/bin/env python3
"""
Cron expression parser and validator.
Usage:
python parse-cron.py "0 9 * * 1-5"
python parse-cron.py --next 5 "0 9 * * 1-5"
python parse-cron.py --human "0 9 * * 1-5"
"""
import sys
import re
from datetime import datetime, timedelta
from typing import List, Optional, Tuple
DAYS_OF_WEEK = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday']
MONTHS = ['', 'January', 'February', 'March', 'April', 'May', 'June',
'July', 'August', 'September', 'October', 'November', 'December']
def parse_field(field: str, min_val: int, max_val: int) -> List[int]:
"""Parse a cron field into a list of valid values."""
values = set()
for part in field.split(','):
if part == '*':
values.update(range(min_val, max_val + 1))
elif '/' in part:
base, step = part.split('/')
step = int(step)
if base == '*':
start = min_val
else:
start = int(base.split('-')[0])
values.update(range(start, max_val + 1, step))
elif '-' in part:
start, end = map(int, part.split('-'))
values.update(range(start, end + 1))
else:
values.add(int(part))
return sorted(v for v in values if min_val <= v <= max_val)
def validate_cron(expression: str) -> Tuple[bool, Optional[str]]:
"""Validate a cron expression."""
parts = expression.strip().split()
if len(parts) != 5:
return False, f"Expected 5 fields, got {len(parts)}"
field_specs = [
('minute', 0, 59),
('hour', 0, 23),
('day of month', 1, 31),
('month', 1, 12),
('day of week', 0, 7), # 7 is also Sunday
]
for i, (name, min_val, max_val) in enumerate(field_specs):
try:
values = parse_field(parts[i], min_val, max_val)
if not values:
return False, f"Invalid {name}: {parts[i]}"
except Exception as e:
return False, f"Invalid {name}: {parts[i]} ({e})"
return True, None
def humanize_cron(expression: str) -> str:
"""Convert cron expression to human-readable string."""
parts = expression.strip().split()
if len(parts) != 5:
return expression
minute, hour, dom, month, dow = parts
# Build description
desc_parts = []
# Time
if minute == '*' and hour == '*':
desc_parts.append("Every minute")
elif minute.startswith('*/'):
desc_parts.append(f"Every {minute[2:]} minutes")
elif hour == '*':
desc_parts.append(f"At minute {minute} of every hour")
elif minute == '0':
if hour.startswith('*/'):
desc_parts.append(f"Every {hour[2:]} hours")
else:
hours = parse_field(hour, 0, 23)
times = [f"{h}:00" for h in hours]
desc_parts.append(f"At {', '.join(times)}")
else:
hours = parse_field(hour, 0, 23)
mins = parse_field(minute, 0, 59)
times = [f"{h}:{m:02d}" for h in hours for m in mins]
desc_parts.append(f"At {', '.join(times[:3])}" + ("..." if len(times) > 3 else ""))
# Day of week
if dow != '*':
days = parse_field(dow, 0, 7)
# Normalize Sunday (7 -> 0)
days = [d % 7 for d in days]
days = sorted(set(days))
if days == [1, 2, 3, 4, 5]:
desc_parts.append("on weekdays")
elif days == [0, 6]:
desc_parts.append("on weekends")
elif len(days) == 1:
desc_parts.append(f"on {DAYS_OF_WEEK[days[0]]}")
else:
day_names = [DAYS_OF_WEEK[d] for d in days]
desc_parts.append(f"on {', '.join(day_names)}")
# Day of month
elif dom != '*':
days = parse_field(dom, 1, 31)
if len(days) == 1:
desc_parts.append(f"on day {days[0]} of the month")
else:
desc_parts.append(f"on days {', '.join(map(str, days[:3]))}" + ("..." if len(days) > 3 else ""))
# Month
if month != '*':
months = parse_field(month, 1, 12)
month_names = [MONTHS[m] for m in months]
desc_parts.append(f"in {', '.join(month_names)}")
return " ".join(desc_parts)
def get_next_runs(expression: str, count: int = 5) -> List[datetime]:
"""Calculate the next N run times for a cron expression."""
parts = expression.strip().split()
if len(parts) != 5:
return []
minutes = parse_field(parts[0], 0, 59)
hours = parse_field(parts[1], 0, 23)
doms = parse_field(parts[2], 1, 31)
months = parse_field(parts[3], 1, 12)
dows = parse_field(parts[4], 0, 7)
dows = [d % 7 for d in dows] # Normalize Sunday
runs = []
current = datetime.now().replace(second=0, microsecond=0)
# Look ahead up to 2 years
end_date = current + timedelta(days=730)
while len(runs) < count and current < end_date:
current += timedelta(minutes=1)
if current.minute not in minutes:
continue
if current.hour not in hours:
continue
if current.month not in months:
continue
# Check day (dom OR dow)
dom_match = parts[2] == '*' or current.day in doms
dow_match = parts[4] == '*' or current.weekday() in [(d - 1) % 7 for d in dows] or current.weekday() == 6 and 0 in dows
if dom_match or dow_match:
runs.append(current)
return runs
def main():
if len(sys.argv) < 2:
print("Usage: python parse-cron.py [--next N] [--human] <cron-expression>")
print()
print("Examples:")
print(" python parse-cron.py '0 9 * * 1-5'")
print(" python parse-cron.py --next 5 '0 9 * * *'")
print(" python parse-cron.py --human '*/15 * * * *'")
sys.exit(1)
args = sys.argv[1:]
show_next = 0
show_human = False
expression = None
i = 0
while i < len(args):
if args[i] == '--next':
show_next = int(args[i + 1])
i += 2
elif args[i] == '--human':
show_human = True
i += 1
else:
expression = args[i]
i += 1
if not expression:
print("Error: No cron expression provided")
sys.exit(1)
# Validate
valid, error = validate_cron(expression)
if not valid:
print(f"Invalid cron expression: {error}")
sys.exit(1)
print(f"Expression: {expression}")
print(f"Valid: Yes")
# Human readable
human = humanize_cron(expression)
print(f"Description: {human}")
# Next runs
if show_next > 0 or not show_human:
runs = get_next_runs(expression, show_next or 5)
print(f"\nNext {len(runs)} runs:")
for run in runs:
print(f" {run.strftime('%a %b %d %Y %H:%M')}")
if __name__ == '__main__':
main()
#!/bin/bash
# Linux crontab setup helper
# Usage: ./setup-crontab.sh <task-id> <cron-expression> <command> [working-dir]
set -e
TASK_ID="${1:?Task ID required}"
CRON_EXPR="${2:?Cron expression required}"
COMMAND="${3:?Command required}"
WORK_DIR="${4:-$(pwd)}"
LOG_DIR="$HOME/.claude/logs"
MARKER="# claude-scheduler:${TASK_ID}"
# Ensure log directory exists
mkdir -p "$LOG_DIR"
# Get current crontab (or empty if none)
CURRENT_CRONTAB=$(crontab -l 2>/dev/null || echo "")
# Remove existing entry for this task
NEW_CRONTAB=$(echo "$CURRENT_CRONTAB" | grep -v "$MARKER" || true)
# Add new entry
CRON_LINE="${CRON_EXPR} cd \"${WORK_DIR}\" && ${COMMAND} >> \"${LOG_DIR}/${TASK_ID}.log\" 2>&1 ${MARKER}"
# Combine and set new crontab
echo -e "${NEW_CRONTAB}\n${CRON_LINE}" | grep -v '^$' | crontab -
echo "Added crontab entry for task: ${TASK_ID}"
echo "Schedule: ${CRON_EXPR}"
echo "Command: ${COMMAND}"
echo "Log file: ${LOG_DIR}/${TASK_ID}.log"
echo ""
echo "Current crontab:"
crontab -l | grep -v '^#' | head -10
#!/bin/bash
# macOS launchd setup helper
# Usage: ./setup-launchd.sh <task-id> <cron-expression> <command> [working-dir]
set -e
TASK_ID="${1:?Task ID required}"
CRON_EXPR="${2:?Cron expression required}"
COMMAND="${3:?Command required}"
WORK_DIR="${4:-$(pwd)}"
LABEL="com.claude.scheduler.${TASK_ID}"
PLIST_PATH="$HOME/Library/LaunchAgents/${LABEL}.plist"
LOG_DIR="$HOME/.claude/logs"
# Ensure directories exist
mkdir -p "$HOME/Library/LaunchAgents"
mkdir -p "$LOG_DIR"
# Parse cron expression (minute hour dom month dow)
IFS=' ' read -r MINUTE HOUR DOM MONTH DOW <<< "$CRON_EXPR"
# Build calendar interval
build_calendar_interval() {
local interval="<key>StartCalendarInterval</key>\n <dict>\n"
if [[ "$MINUTE" != "*" ]]; then
interval+=" <key>Minute</key>\n <integer>${MINUTE}</integer>\n"
fi
if [[ "$HOUR" != "*" ]]; then
interval+=" <key>Hour</key>\n <integer>${HOUR}</integer>\n"
fi
if [[ "$DOM" != "*" ]]; then
interval+=" <key>Day</key>\n <integer>${DOM}</integer>\n"
fi
if [[ "$MONTH" != "*" ]]; then
interval+=" <key>Month</key>\n <integer>${MONTH}</integer>\n"
fi
if [[ "$DOW" != "*" ]]; then
interval+=" <key>Weekday</key>\n <integer>${DOW}</integer>\n"
fi
interval+=" </dict>"
echo -e "$interval"
}
CALENDAR_INTERVAL=$(build_calendar_interval)
# Escape command for XML
escape_xml() {
echo "$1" | sed 's/&/\&/g; s/</\</g; s/>/\>/g; s/"/\"/g'
}
ESCAPED_COMMAND=$(escape_xml "$COMMAND")
ESCAPED_WORKDIR=$(escape_xml "$WORK_DIR")
# Generate plist
cat > "$PLIST_PATH" << EOF
<?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>${LABEL}</string>
<key>ProgramArguments</key>
<array>
<string>/bin/bash</string>
<string>-c</string>
<string>cd "${ESCAPED_WORKDIR}" && ${ESCAPED_COMMAND}</string>
</array>
${CALENDAR_INTERVAL}
<key>StandardOutPath</key>
<string>${LOG_DIR}/${TASK_ID}.out.log</string>
<key>StandardErrorPath</key>
<string>${LOG_DIR}/${TASK_ID}.err.log</string>
<key>RunAtLoad</key>
<false/>
<key>EnvironmentVariables</key>
<dict>
<key>PATH</key>
<string>/usr/local/bin:/usr/bin:/bin:/opt/homebrew/bin:${HOME}/.local/bin</string>
</dict>
</dict>
</plist>
EOF
echo "Created plist: $PLIST_PATH"
# Unload if already loaded
launchctl unload "$PLIST_PATH" 2>/dev/null || true
# Load the agent
launchctl load "$PLIST_PATH"
echo "Loaded agent: $LABEL"
echo "Log files: $LOG_DIR/${TASK_ID}.{out,err}.log"
# Windows Task Scheduler setup helper
# Usage: .\setup-schtasks.ps1 -TaskId "task-id" -CronExpr "0 9 * * 1-5" -Command "claude -p /review" [-WorkDir "C:\path"]
param(
[Parameter(Mandatory=$true)]
[string]$TaskId,
[Parameter(Mandatory=$true)]
[string]$CronExpr,
[Parameter(Mandatory=$true)]
[string]$Command,
[string]$WorkDir = (Get-Location).Path
)
$TaskFolder = "\ClaudeScheduler"
$TaskName = "$TaskFolder\$TaskId"
$LogDir = "$env:USERPROFILE\.claude\logs"
# Ensure log directory exists
New-Item -ItemType Directory -Force -Path $LogDir | Out-Null
# Parse cron expression
$parts = $CronExpr -split '\s+'
if ($parts.Count -ne 5) {
Write-Error "Invalid cron expression. Expected 5 fields."
exit 1
}
$minute, $hour, $dom, $month, $dow = $parts
# Determine schedule type and parameters
function Get-ScheduleParams {
# Every N minutes
if ($minute -match '^\*/(\d+)$') {
return @{
Schedule = "MINUTE"
Modifier = $Matches[1]
}
}
# Every N hours
if ($hour -match '^\*/(\d+)$') {
return @{
Schedule = "HOURLY"
Modifier = $Matches[1]
}
}
# Time for daily/weekly schedules
$time = $null
if ($minute -ne '*' -and $hour -ne '*') {
$h = [int]$hour
$m = [int]$minute
$time = "{0:D2}:{1:D2}" -f $h, $m
}
# Weekly (specific day of week)
if ($dow -ne '*' -and $dom -eq '*') {
$dayMap = @{
'0' = 'SUN'; '1' = 'MON'; '2' = 'TUE'; '3' = 'WED'
'4' = 'THU'; '5' = 'FRI'; '6' = 'SAT'; '7' = 'SUN'
}
$days = @()
if ($dow -match '(\d+)-(\d+)') {
for ($i = [int]$Matches[1]; $i -le [int]$Matches[2]; $i++) {
$days += $dayMap[[string]$i]
}
} else {
foreach ($d in $dow -split ',') {
$days += $dayMap[$d]
}
}
return @{
Schedule = "WEEKLY"
Days = $days -join ','
StartTime = $time
}
}
# Monthly (specific day of month)
if ($dom -ne '*') {
return @{
Schedule = "MONTHLY"
Modifier = $dom
StartTime = $time
}
}
# Default to daily
return @{
Schedule = "DAILY"
StartTime = $time
}
}
$schedParams = Get-ScheduleParams
# Delete existing task if present
try {
schtasks /Delete /TN $TaskName /F 2>$null
} catch {}
# Build schtasks command
$logFile = "$LogDir\$TaskId.log"
$taskCommand = "cmd /c `"cd /d `"$WorkDir`" && $Command >> `"$logFile`" 2>&1`""
$args = @(
"/Create"
"/TN", $TaskName
"/TR", $taskCommand
"/SC", $schedParams.Schedule
)
if ($schedParams.Modifier) {
$args += @("/MO", $schedParams.Modifier)
}
if ($schedParams.StartTime) {
$args += @("/ST", $schedParams.StartTime)
}
if ($schedParams.Days) {
$args += @("/D", $schedParams.Days)
}
# Create the task
Write-Host "Creating scheduled task: $TaskName"
Write-Host "Command: schtasks $($args -join ' ')"
& schtasks @args
Write-Host ""
Write-Host "Task created successfully!"
Write-Host "Log file: $logFile"
Write-Host ""
Write-Host "To run the task manually:"
Write-Host " schtasks /Run /TN `"$TaskName`""
Write-Host ""
Write-Host "To delete the task:"
Write-Host " schtasks /Delete /TN `"$TaskName`" /F"
Related skills
How it compares
Use scheduler for classic five-field cron strings when you do not need timezone-aware calendar DSLs from heavier orchestrators.
FAQ
What is the cron field order in scheduler?
scheduler uses the standard five-field order: minute (0–59), hour (0–23), day of month (1–31), month (1–12), then day of week (0–6 with Sunday=0).
How does scheduler express multiple run times?
scheduler shows comma lists in the hour field, such as 0 9,17 * * *, which runs at minute 0 of hours 9 and 17 every day.
Is Scheduler safe to install?
skills.sh reports 2 of 3 security scanners passed. Review the Security Audits panel on this page before installing in production.