Schedule recurring AI agent tasks from markdown files.
Each task is a .md file with YAML frontmatter (on, executor, options) and a markdown prompt body. A per-minute heartbeat evaluates which scheduled tasks are due and dispatches them.
Requires Bun and at least one supported agent on PATH (e.g. Claude Code).
bun install
bun run build # compiles to ./tm# Install the per-minute scheduler (launchd on macOS, crontab on Linux)
tm setup
# Create a task
mkdir -p ~/.config/taskmaster/tasks
cat > ~/.config/taskmaster/tasks/daily-audit.md << 'EOF'
---
on:
schedule: '0 8 * * 1-5'
agent: claude
timezone: 'Europe/Paris'
cwd: '~/projects/saas-app'
---
Review package.json for dependencies with known CVEs.
Run `npm audit` and output a short markdown summary.
EOF
# Validate, run, and monitor
tm validate
tm run daily-audit
tm status
tm history daily-audit
tm logs daily-audit
tm doctorTasks live in ~/.config/taskmaster/tasks/ as markdown files.
The filename (minus .md) is the task ID and must match [a-z0-9-]+.
| Field | Required | Default | Description |
|---|---|---|---|
on |
yes | Trigger definition. Exactly one of schedule or event must be set inside it |
|
agent |
one of agent or run |
Agent name (built-in: claude, codex, opencode, pi; or custom from agents.yml) |
|
run |
one of agent or run |
Custom shell command (must reference $TM_PROMPT_FILE) |
|
args |
no | '' |
Extra CLI flags appended to the agent command (only with agent) |
cwd |
no | temp dir | Working directory (~ is expanded) |
timezone |
no | system local | IANA timezone for cron evaluation |
env |
no | {} |
Environment variables (string key-value pairs) |
timeout |
no | scheduled: min(interval, 1h); event: 1h |
Max runtime as duration string (30s, 5m). For scheduled tasks it must be shorter than the schedule interval |
enabled |
no | true |
Lifecycle switch. false = never auto-scheduled. true = eligible to run (subject to requires) |
requires |
no | ['network'] |
Runtime requirements that must be satisfied for the task to run. Empty array [] means no requirements |
preflight |
no | Shell command run before the agent. Stdout substitutes into the <PREFLIGHT/> token. Exit 1 skips the run; any other non-zero is a preflight error |
Constraints:
onmust contain exactly one ofscheduleorevent.- Exactly one of
agentorrunmust be set. argscan only be used withagent, notrun.runmust contain$TM_PROMPT_FILE.timeoutminimum is1s.- For scheduled tasks,
timeoutmust be shorter than the schedule interval. When omitted it defaults tomin(interval, 1h). - For event tasks,
timeoutdefaults to1hwhen omitted. - A
<PREFLIGHT/>token in the body requires apreflightfield. - A
<PAYLOAD/>token in the body is only valid on event tasks.
Scheduled task:
on:
schedule: '*/5 * * * *'Event-driven task:
on:
event: deploy---
on:
schedule: '*/5 * * * *'
agent: claude
---
Check disk usage and report if any partition exceeds 90%.---
on:
schedule: '*/30 9-17 * * 1-5'
timezone: 'America/New_York'
agent: claude
args: '--model sonnet'
cwd: '~/projects/api'
timeout: '5m'
requires: []
env:
GITHUB_TOKEN_SCOPE: 'read'
---
Check for open PRs that have been idle for more than 48 hours.
Post a summary as a comment on each one using `gh`.Use run instead of agent for arbitrary commands:
---
on:
schedule: '0 8 * * 1-5'
run: 'my-tool --prompt $TM_PROMPT_FILE'
---
Generate the weekly status report.Gate a power-hungry local-model task on wall power so it does not drain the battery:
---
on:
schedule: '0 */2 * * *'
agent: opencode
requires: ['ac-power']
---
Run the expensive local embedding pass over the inbox.Use tm dispatch <event> to trigger tasks subscribed to an event. Stdin piped to tm dispatch becomes the event payload, available to the task via the <PAYLOAD/> token (or, for run commands, the $TM_EVENT_PAYLOAD_FILE env variable):
---
on:
event: deploy
agent: claude
requires: []
---
Summarize this deployment payload and post release notes:
<PAYLOAD/>echo '{"version":"1.2.3","commit":"abc123"}' | tm dispatch deployA preflight command runs before the agent. Its stdout (UTF-8, ≤ 1 MiB) is substituted into the <PREFLIGHT/> token in the prompt body. The agent is only spawned when preflight exits 0.
| Preflight outcome | Result |
|---|---|
Exit 0 |
Agent runs; stdout fills <PREFLIGHT/> |
Exit 1 (clean) |
Run skipped (skipped-preflight) |
| Other non-zero / signal / timeout (60s) / non-UTF-8 / oversize stdout | Run aborted (preflight-error) |
---
on:
schedule: '*/15 * * * *'
agent: claude
preflight: 'gh pr list --json number,title,updatedAt --search "is:open updated:<$(date -v-2d -u +%Y-%m-%dT%H:%M:%SZ)"'
---
Review these idle PRs and suggest follow-ups:
<PREFLIGHT/>All commands except doctor and logs support --json for structured output.
tm run <name> Execute a task immediately (bypasses enabled and requires)
tm list One line per task: name, trigger, executor, enabled status, [preflight]
tm status Rich view with last run, next scheduled time, running marker
tm history [name] Show run history for a task, or across all tasks (--failures, --last <n>)
tm logs <name> Live-tail output if running, otherwise print last completed output
tm dispatch <event> Dispatch all tasks subscribed to an event (stdin → payload)
tm validate Check all task files for errors
tm doctor Run diagnostics (--since <iso8601>, default: 7 days)
tm setup Install system scheduler (launchd/crontab)
tm teardown Remove system scheduler
tm tick Scheduler heartbeat (--dry-run to preview without executing)
Checks system health and reports findings by severity:
- Heartbeat staleness — scheduler not firing (critical if > 5 min)
- Scheduler installation — launchd/crontab missing
- Task validation errors — malformed task files
- Consecutive failures — 3+ failures = critical
- Consecutive timeouts — 3+ timeouts = critical
- Never-ran tasks — enabled tasks with no history
- Lock contention — concurrent execution attempts
- Offline skips — tasks skipped due to connectivity
- Chronically blocked tasks — 3+ consecutive skips for the same unmet requirement
- Timeout/schedule mismatch — timeout >= schedule interval
- Chronic preflight errors — 3+ consecutive
preflight-erroroutcomes (critical) - Stale preflight success — preflight task has not had a successful agent run in 14+ days (info)
Built-in agents and their dispatch commands:
| Agent | Command |
|---|---|
claude |
claude -p < $TM_PROMPT_FILE |
codex |
codex exec - < $TM_PROMPT_FILE |
opencode |
opencode run -f $TM_PROMPT_FILE |
pi |
pi -p @$TM_PROMPT_FILE |
$TM_PROMPT_FILE is set at runtime to a temp file containing the resolved prompt body (frontmatter stripped, tokens substituted).
Override built-in agents or add new ones in ~/.config/taskmaster/agents.yml:
claude: claude --model sonnet -p < $TM_PROMPT_FILE
my-agent: my-agent --prompt-file $TM_PROMPT_FILECustom entries merge on top of the built-in registry. Every template must reference $TM_PROMPT_FILE.
The prompt body supports two self-closing tokens that get substituted just before the agent runs:
| Token | Source | Notes |
|---|---|---|
<PREFLIGHT/> |
Stdout of the preflight command |
Trimmed; UTF-8, ≤ 1 MiB |
<PAYLOAD/> |
Stdin piped to tm dispatch <event> |
Trimmed; UTF-8, ≤ 1 MiB; event tasks only. Empty if no payload was provided |
Substitution is single-pass (replacement strings are not re-scanned). When at least one token resolves to non-empty content, the resolved prompt is persisted to history as <timestamp>.prompt.txt.
Variables resolve in order (last wins):
- System environment
~/.config/taskmaster/.env(global,KEY=VALUEformat)- Per-task
envfrontmatter - Runtime
TM_*variables (set by Taskmaster, see below)
Set in the agent and preflight environment:
| Variable | When set | Description |
|---|---|---|
TM_TASK_NAME |
always | Task name |
TM_PROMPT_FILE |
agent only | Path to temp file containing the resolved prompt |
TM_TRIGGER |
when known | manual, tick, or dispatch |
TM_RUN_TIMESTAMP |
scheduled / dispatched runs | UTC timestamp identifying the run |
TM_EVENT_NAME |
dispatch only | Event name |
TM_EVENT_PAYLOAD_FILE |
dispatch with payload | Path to the per-task payload file |
~/.config/taskmaster/
tasks/ Task markdown files (*.md)
history/ Per-task run history
<task>/
<timestamp>.meta.json Run metadata (status, durations, exit code, preflight block)
<timestamp>.output.txt Agent stdout/stderr
<timestamp>.prompt.txt Resolved prompt (only when a token produced content)
<timestamp>.preflight.txt Preflight stdout + stderr (only when preflight ran)
locks/ Per-task lock + running-marker files (runtime)
runs/ Preserved temp dirs from failed runs
log.jsonl Structured event log
.env Global environment variables (optional)
agents.yml Custom agent definitions (optional)
heartbeat Timestamp of last tick
Tasks declare what the environment must provide via requires. Each token has a matching probe; the scheduler probes each referenced requirement at most once per tick, in parallel, and only when at least one ready task references it.
Valid tokens:
| Token | Meaning | Probe |
|---|---|---|
network |
Internet reachable | DNS lookup against Cloudflare (1.1.1.1) and Google (8.8.8.8), 2s timeout |
ac-power |
Running on wall power (not battery) | macOS: pmset -g ps; Linux: /sys/class/power_supply/*/online for type=Mains. Fails open: probe errors, unexpected output, or absent Mains source (desktops) all count as satisfied. Windows is not supported — fails open. |
Defaults and semantics:
- Omitting
requiresdefaults to['network']— preserves today's behavior. - Explicit
requires: []means "no runtime requirements — always runs" (still subject toenabled). - Unknown tokens fail validation at parse time.
- Entries are deduplicated automatically.
- Tasks with unmet requirements are skipped for the tick and logged as
{ event: 'skipped', reason: 'requirement-unmet', requirement: [...] }. tm run <name>bypasses bothenabledandrequires.
Event-driven tasks dispatched with tm dispatch honor requires identically to scheduled tasks.
bun test # run tests
bun run check # fmt + lint + typecheck + test + knip + deprecated check
bun run lint # oxlint
bun run fmt # oxfmt
bun run typecheck # tsgo
bun run test:integration # *.integration-test.ts onlyMIT