Scheduler
A cron-like rule engine embedded in the crew dashboard for periodic tasks and agent lifecycle automation. Rules are declarative, one per line, stored in a plain text file per crew.
When To Use It
- Keep a specific agent alive automatically (re-summon on crash or release)
- Send periodic messages (daily narrative prompts, status checks)
- Release idle agents after a timeout
- Run shell commands on a schedule (test suites, maintenance scripts)
- Replace system crontab entries for crew-level operations
Quick Start
Create a rules file at .crew/scheduler.rules in the crew directory:
[keep-forge] every 1m if not alive(forge) => summon forge
[daily-report] at 0 0 * * * => msg beacon "Write daily narrative for today"
The dashboard picks up the file automatically on the next tick (within 10 seconds). No restart required.
Verify with a dry run:
$ metateam crew scheduler test
Rule Format
[<name>] <trigger> [<condition>] => <action>
- Name (optional): enclosed in
[brackets], must be unique. If omitted, an auto-generated name is derived from the trigger and action. - Trigger: when the rule fires (interval or cron).
- Condition (optional): a predicate that must be true for the action to execute.
- Action: what to do when the rule fires.
- Parts are separated by
=>(space-arrow-space). - Empty lines and
#comments are ignored.
Triggers
Interval
every <duration>
Fires repeatedly at fixed intervals. Duration format: <number><unit> where unit is s (seconds), m (minutes), h (hours), or d (days).
After restart, an unchanged rule resumes from its matching saved start time. An overdue rule can fire once at the first tick, subject to its condition and failure backoff. Missed intervals are not replayed. The next interval starts from that firing or a consumed false condition or run overlap.
New rules and live content edits wait a full interval. Missing or malformed saved starts use a fresh interval; future starts use zero elapsed time. Completion time does not change the interval baseline. Unit conversion rejects overflow. A valid duration outside the UTC timestamp range has no next-fire estimate.
every 30s
every 5m
every 1h
Cron
Standard 5-field crontab expression, evaluated in the server's local timezone:
at <minute> <hour> <day-of-month> <month> <day-of-week>
Fields support *, */N, N,M, and N-M ranges.
at 0 0 * * * # midnight daily
at */30 * * * * # every 30 minutes
at 0 9,17 * * 1-5 # 9am and 5pm on weekdays
Cron Shorthands
at 14:30 # daily at 14:30
at Sun 00:00 # weekly on Sunday at midnight
Conditions
Optional. If omitted, the action fires unconditionally on trigger. One condition per rule.
alive / not alive
if alive(<agent>)
if not alive(<agent>)
True when the specified agent is or is not present in the communicator agent registry.
Multiple agents use AND semantics:
| Expression | Meaning |
|---|---|
alive(forge) |
Forge is registered |
alive(forge,beacon) |
Forge AND Beacon are both registered |
not alive(forge,beacon) |
Forge OR Beacon is not registered |
idle
if idle(<target>) > <duration>
True when agents have had no real terminal activity for longer than the given duration.
| Target | Meaning |
|---|---|
all |
Every registered agent is idle |
any |
At least one registered agent is idle |
<name> |
Specific agent is idle |
<name1>,<name2> |
All listed agents are idle |
When an idle condition matches, two template variables become available in the action's message:
{idle}-- comma-separated names of idle agents{idle_duration}-- formatted duration of the longest idle agent
Actions
msg
msg <target> "<message>"
Send a message via the communicator. Target: all, <name>, or <name1>,<name2>.
Messages support template variables: {timestamp} (ISO timestamp), {crew} (crew name), {idle} and {idle_duration} (when an idle condition is present).
Messages are suppressed when dashboard silence mode is active.
summon
summon <name> ["<message>"]
Summon a persona. Uses the same path as metateam crew summon -- reads KB persona entry for client, model, and effort defaults. Message is optional.
Summon dedup prevents overlapping summons for the same persona. If a summon is already pending (within a 60-second window), subsequent summon actions for that persona are skipped.
release
release <target>
Release an agent. Target: <name>, <name1>,<name2>, or all.
Not suppressed by silence mode.
run
run "<shell command>"
Execute a shell command asynchronously. Output is logged, not sent to agents.
- Working directory: crew root (repository root)
- Default timeout: 5 minutes (SIGTERM, then SIGKILL after 10 seconds)
- Single-instance per rule: skipped if a previous run is still active
- Last 20 lines of stdout/stderr are captured and logged with the exit code
Complete Examples
# === Agent Lifecycle ===
# Keep an agent alive -- re-summon if it disappears
[keep-forge] every 1m if not alive(forge) => summon forge
# Release agents idle for 4 hours
[idle-release] every 5m if idle(any) > 4h => release {idle}
# === Scheduled Tasks ===
# Daily narrative prompt at midnight
[daily-narrative] at 0 0 * * * => msg beacon "Write daily narrative for today"
# Weekly summary on Sunday at 1am
[weekly-summary] at 0 1 * * 0 => summon beacon "Build weekly narrative"
# === Health Checks ===
# Run tests every hour during work hours on weekdays
[api-tests] at 0 9-17 * * 1-5 => run "dotnet test api/Tests -v minimal"
# === Notifications ===
# Nudge crew lead when everyone is idle
[idle-nudge] every 30m if idle(all) > 10m => msg drift "All agents idle for {idle_duration}"
Dry Run
Parse rules and evaluate conditions against the current crew state without executing actions. Cron checks the current local minute. Interval rules are treated as ready for condition evaluation; this command does not inspect the live interval deadline, backoff, pending summons or retained cron minute dedup:
$ metateam crew scheduler test
The default output is a text table. Use --json for one structured result:
$ metateam crew scheduler test --json
For successful invocations dispatched to this command, JSON stdout contains
one object, including when rules are missing, empty or contain parse errors.
The object reports crew, rules_path, rules_file_present,
intervals_assumed_ready, agents, agents_observed, agents_error,
parse_errors and rules. Parse errors contain line_number, raw_line and
message. Each rule contains line_number, raw_line, its full name,
trigger, condition, action, trigger_ready, condition_matched and
would_fire. Rule and error arrays keep file order; agent names are sorted.
Text can shorten a long name for display; JSON preserves full names and rules.
rules_path uses the same text representation as human output. Path bytes or
code units that cannot form Unicode appear as replacement characters.
agents_observed is true only after a successful roster query. A failed query
uses an empty roster for evaluation and reports its error in agents_error.
Missing rules or input with no valid rules require no roster query and report
false with no error. A successful empty roster reports true. would_fire
describes this simulation, with intervals assumed ready; it does not predict
the live scheduler's next dispatch.
Rules must be UTF-8 text in a regular file of at most 1,048,576 bytes. Invalid UTF-8, oversized or nonregular input and other read errors exit nonzero with an error on stderr. They do not produce a successful JSON result. Missing rules remain a successful empty result.
Shared startup suppression runs before this command. An effective
.nometateam marker prints File <marker-path> was found, exiting. and exits
0, including with --json; it emits no dry-run result. A closer .yesmetateam
marker overrides an ancestor .nometateam marker and restores dispatch.
Dashboard Integration
Status Bar
The scheduler shows a compact indicator in the dashboard status bar:
| Indicator | Meaning |
|---|---|
SCH 3 |
3 rules loaded, all healthy |
SCH 3 !1 |
3 rules, 1 in failure backoff (yellow) |
SCH X |
Scheduler disabled after repeated crashes (red) |
SCH -- |
No rules file or empty |
Notifications
Events that need attention surface as dashboard notification toasts:
- Rule enters backoff after consecutive failures
- Scheduler disabled (crash threshold exceeded)
- Rules file parse errors on reload
runaction timeout or non-zero exit
Normal successful fires are silent.
/scheduler Overlay
The /scheduler slash command opens a full state table:
Name Trigger Condition Last Status
keep-forge every 1m not alive(forge) 30s ago ok
daily-narrative at 0 0 * * * -- 8h ago ok
idle-nudge every 30m idle(all) > 10m 2m ago skip
api-tests at 0 9-17 -- 1h ago !3 bk
Status values: ok (last success), skip (condition was false), !N bk (in backoff with N failures), run (currently executing), abandoned (from unclean restart).
Runtime Behavior
- The scheduler runs inside the dashboard communicator server, ticking every 10 seconds.
- Actions are dispatched asynchronously and never block the tick loop.
- All conditions in a single tick evaluate against the same immutable snapshot of agent state.
- On consecutive failures for the same rule, backoff increases exponentially: 1m, 2m, 4m, up to 1 hour max. Counts saturate at the maximum unsigned 32-bit value. Success resets the count.
- The first observed roster is a baseline, including when it is empty or no rule is loaded. It preserves restored backoff. Later observed arrivals or departures of relevant agents reset backoff. Transitions during downtime cannot be inferred; pause and reload retain the observed baseline.
- Interval firing, backoff status and the next-fire estimate share one elapsed clock. A failing interval waits for the larger of its period and its backoff.
- Execution state is persisted to
.crew/scheduler.state.jsonand survives dashboard restarts. - After downtime, a matching overdue interval gets one evaluation at the first tick. Missed intervals are not replayed. Cron retains its local-minute matching behavior.
- A failed start-state write still dispatches the action and marks the scheduler unhealthy. Another restart can repeat that unrecorded action.
Hot Reload
The scheduler checks .crew/scheduler.rules for changes every tick. On change:
- The file is re-parsed entirely.
- Unchanged rules retain their complete elapsed clock and execution state. Matching requires name and normalized content hash; unnamed live rules also match by hash after reordering.
- Live content edits start fresh. A rule absent from the live set can resume matching saved history when loaded again.
- A reload with parse errors keeps the previously admitted rules. An initial load admits valid rules and reports the errors.
Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
| Rule never fires | Condition always false | Run metateam crew scheduler test to see current evaluation |
| Rule fires repeatedly | Condition flaps (agent keeps dying) | Check agent health; backoff will engage automatically |
summon action skipped |
Another summon for that agent is pending | Wait for the 60-second dedup window to expire |
Status shows !N bk |
N consecutive failures triggered backoff | Fix the underlying issue; backoff resets on success |
Status shows abandoned |
Dashboard crashed mid-execution | State resets on next successful fire |
| Parse error on reload | Syntax error in rules file | Correct the reported line; previously admitted rules keep running |
Guarantees vs Best-Effort
| Aspect | Level |
|---|---|
| Rule evaluation order | Guaranteed: file order, top to bottom |
| Live interval/minute dedup | Guaranteed for an unchanged rule runtime while it stays loaded; cron dedup holds while the current minute key stays the same |
| Exact fire time | Best-effort: evaluated every 10 seconds, so up to 10s late |
| Overdue interval after restart | One evaluation: condition and backoff apply; missed intervals are not replayed |
| Action completion | Best-effort: actions can fail; failures trigger backoff |
| State persistence across restart | Published state survives restart; failed writes are best effort and can leave older history |
Removing and restoring a rule, or editing its content, creates fresh cron dedup state. The rule can become eligible again within the same minute, subject to its condition and backoff.
See Also
- Crew (Command Reference) -- summon, release, message commands
- The Terminal Dashboard -- dashboard overview
- Start & Session Upload -- startup context and manual session upload catch-up