Cron Scheduling
ClawDesk includes a built-in cron scheduling system for running automated tasks—daily summaries, periodic health checks, scheduled messages, and more. Tasks run through the agent pipeline with full access to tools, memory, and channels.
Architecture
Key Components
| Component | Description |
|---|---|
CronManager | Top-level orchestrator; manages task lifecycle, scheduling, and state |
CronExecutor | Executes tasks in isolated contexts with timeout enforcement |
Heartbeat | Monitors task health and detects stuck/hung executions |
| Schedule Parser | Parses cron expressions (standard + extended syntax) |
| Overlap Guard | Prevents concurrent executions of the same task |
Schedule Syntax
ClawDesk supports standard cron syntax and convenient shorthand expressions:
Standard Cron Format
┌───────── minute (0–59)
│ ┌───────── hour (0–23)
│ │ ┌───────── day of month (1–31)
│ │ │ ┌───────── month (1–12 or JAN–DEC)
│ │ │ │ ┌───────── day of week (0–7 or SUN–SAT, 0 and 7 = Sunday)
│ │ │ │ │
* * * * *
Examples
| Expression | Description |
|---|---|
0 9 * * * | Every day at 9:00 AM |
*/15 * * * * | Every 15 minutes |
0 9 * * MON-FRI | Weekdays at 9:00 AM |
0 0 1 * * | First day of every month at midnight |
30 14 * * WED | Every Wednesday at 2:30 PM |
0 */4 * * * | Every 4 hours |
0 8,12,17 * * * | At 8 AM, 12 PM, and 5 PM daily |
0 9 1,15 * * | 1st and 15th of each month at 9 AM |
Shorthand Expressions
| Shorthand | Equivalent | Description |
|---|---|---|
@yearly / @annually | 0 0 1 1 * | Once a year (Jan 1) |
@monthly | 0 0 1 * * | First of every month |
@weekly | 0 0 * * 0 | Every Sunday |
@daily / @midnight | 0 0 * * * | Every day at midnight |
@hourly | 0 * * * * | Every hour |
@every 30m | Every 30 minutes | |
@every 2h | Every 2 hours | |
@every 1d | Every 24 hours |
Use @every for simple intervals. It's easier to read and supports arbitrary durations:
schedule = "@every 45m"
Task Configuration
Defining Tasks in TOML
# A daily summary task
[[cron.tasks]]
id = "daily-summary"
name = "Daily Conversation Summary"
schedule = "0 9 * * MON-FRI"
enabled = true
channel = "tg_admin"
timezone = "America/New_York"
[cron.tasks.message]
text = """
Generate a summary of all conversations from the past 24 hours.
Include: total messages, active channels, common topics, and any unresolved issues.
"""
provider = "anthropic"
model = "claude-sonnet-4-20250514"
tools = ["knowledge_base"]
[cron.tasks.execution]
timeout_secs = 60
max_retries = 2
overlap_policy = "skip" # "skip" | "queue" | "cancel_previous"
# A periodic health check
[[cron.tasks]]
id = "health-check"
name = "System Health Check"
schedule = "@every 30m"
enabled = true
channel = "tg_admin"
[cron.tasks.message]
text = "Run a system health check and report any issues."
tools = ["system_status", "metrics_query"]
[cron.tasks.execution]
timeout_secs = 30
overlap_policy = "skip"
# A weekly report
[[cron.tasks]]
id = "weekly-report"
name = "Weekly Analytics Report"
schedule = "0 10 * * MON"
enabled = true
channel = "sl_work"
[cron.tasks.message]
text = """
Generate a weekly analytics report covering:
1. Message volume by channel
2. Provider usage and costs
3. Error rates and top errors
4. User engagement metrics
Format as a Slack-friendly message with sections.
"""
tools = ["metrics_query", "cost_tracker"]
[cron.tasks.execution]
timeout_secs = 120
max_retries = 3
Via CLI
# Add a task interactively
clawdesk cron add
# Add a task with flags
clawdesk cron add \
--id "daily-summary" \
--name "Daily Summary" \
--schedule "0 9 * * MON-FRI" \
--channel tg_admin \
--message "Summarize yesterday's conversations" \
--timezone "America/New_York"
# List all tasks
clawdesk cron list
Example output of clawdesk cron list:
┌────────────────┬──────────────────────┬─────────────────┬─────────┬──────────────────┬───────────┐
│ ID │ Name │ Schedule │ Status │ Last Run │ Next Run │
├────────────────┼──────────────────────┼─────────────────┼─────────┼──────────────────┼───────────┤
│ daily-summary │ Daily Summary │ 0 9 * * MON-FRI │ Active │ 2026-02-16 09:00 │ in 14h │
│ health-check │ Health Check │ @every 30m │ Active │ 2 min ago │ in 28m │
│ weekly-report │ Weekly Report │ 0 10 * * MON │ Active │ 2026-02-10 10:00 │ in 5d │
│ cleanup │ Session Cleanup │ 0 3 * * * │ Paused │ 2026-02-15 03:00 │ — │
└────────────────┴──────────────────────┴─────────────────┴─────────┴──────────────────┴───────────┘
Execution Model
Isolated Execution
Each cron task runs in an isolated execution context:
pub struct CronExecutor {
pipeline: Arc<AgentPipeline>,
channel_registry: Arc<ChannelRegistry>,
}
impl CronExecutor {
pub async fn execute(&self, task: &CronTask) -> Result<ExecutionResult> {
// Create an isolated context for this execution
let ctx = ExecutionContext::new()
.with_task_id(&task.id)
.with_timeout(task.execution.timeout)
.with_provider_override(task.message.provider.as_deref())
.with_model_override(task.message.model.as_deref());
// Build an inbound message from the task
let message = InboundMessage::from_cron(task);
// Process through the agent pipeline
let response = tokio::time::timeout(
task.execution.timeout,
self.pipeline.process_with_context(message, ctx),
).await??;
// Send to the target channel
if let Some(channel_id) = &task.channel {
let channel = self.channel_registry.get(channel_id)?;
channel.send(response.into()).await?;
}
Ok(ExecutionResult {
task_id: task.id.clone(),
started_at: Instant::now(),
duration: response.duration,
status: ExecutionStatus::Success,
output_preview: response.text_preview(200),
})
}
}
Overlap Prevention
Three overlap policies control what happens when a task is triggered while a previous execution is still running:
| Policy | Behavior |
|---|---|
skip | Skip the new execution, keep the running one (default) |
queue | Queue the new execution to run after the current one finishes |
cancel_previous | Cancel the running execution and start a new one |
[cron.tasks.execution]
overlap_policy = "skip"
Use cancel_previous with caution. If a task involves tool calls with side effects (e.g., sending emails, creating tickets), cancelling mid-execution may leave operations in an incomplete state.
Timeout Enforcement
Every cron task has a configurable timeout:
[cron.tasks.execution]
timeout_secs = 60 # per-execution timeout
max_retries = 2 # retry on failure
retry_delay_secs = 10 # delay between retries
If a task exceeds its timeout, it's forcefully terminated and marked as TimedOut in the execution history.
Heartbeat System
The heartbeat system monitors running tasks and detects hung executions:
pub struct Heartbeat {
interval: Duration,
stale_threshold: Duration,
running_tasks: Arc<DashMap<TaskId, HeartbeatEntry>>,
}
pub struct HeartbeatEntry {
pub task_id: TaskId,
pub started_at: Instant,
pub last_heartbeat: Instant,
pub progress: Option<String>,
}
impl Heartbeat {
/// Start the heartbeat monitor
pub async fn start(&self) {
loop {
tokio::time::sleep(self.interval).await;
for entry in self.running_tasks.iter() {
if entry.last_heartbeat.elapsed() > self.stale_threshold {
tracing::warn!(
task_id = %entry.task_id,
elapsed = ?entry.last_heartbeat.elapsed(),
"Task heartbeat stale — possible hang"
);
// Optionally kill the task
}
}
}
}
/// Record a heartbeat from a running task
pub fn beat(&self, task_id: &TaskId, progress: Option<String>) {
if let Some(mut entry) = self.running_tasks.get_mut(task_id) {
entry.last_heartbeat = Instant::now();
entry.progress = progress;
}
}
}
Configure the heartbeat system:
[cron.heartbeat]
enabled = true
interval_secs = 10 # how often to check
stale_threshold_secs = 60 # time without heartbeat before alert
kill_on_stale = false # kill stale tasks automatically
alert_channel = "tg_admin" # send alerts to this channel
Monitoring
Execution History
# View execution history for a task
clawdesk cron history daily-summary --limit 10
# Example output:
# ┌──────────────────────┬──────────┬──────────┬──────────────────────────────┐
# │ Executed At │ Duration │ Status │ Output Preview │
# ├──────────────────────┼──────────┼──────────┼──────────────────────────────┤
# │ 2026-02-17 09:00:02 │ 4.2s │ ✅ OK │ Yesterday's summary: 142... │
# │ 2026-02-14 09:00:01 │ 3.8s │ ✅ OK │ Thursday summary: 98 me... │
# │ 2026-02-13 09:00:03 │ 12.1s │ ⚠️ Retry │ Timed out, retried succ... │
# │ 2026-02-12 09:00:01 │ 3.5s │ ✅ OK │ Tuesday summary: 76 mes... │
# └──────────────────────┴──────────┴──────────┴──────────────────────────────┘
Metrics
| Metric | Type | Description |
|---|---|---|
clawdesk_cron_executions_total | Counter | Total executions by task and status |
clawdesk_cron_duration_seconds | Histogram | Execution duration by task |
clawdesk_cron_errors_total | Counter | Errors by task and error type |
clawdesk_cron_skipped_total | Counter | Skipped executions (overlap) |
clawdesk_cron_active_tasks | Gauge | Currently running tasks |
clawdesk_cron_heartbeat_stale | Counter | Stale heartbeat events |
Manual Triggers
# Run a task immediately (outside its schedule)
clawdesk cron run daily-summary
# Run with output to terminal
clawdesk cron run daily-summary --output terminal
# Dry run (show what would execute without actually running)
clawdesk cron run daily-summary --dry-run
Advanced Patterns
Chained Tasks
Execute tasks in sequence:
[[cron.tasks]]
id = "etl-extract"
schedule = "0 2 * * *"
# ...
[[cron.tasks]]
id = "etl-transform"
schedule = "" # no schedule — triggered only by chain
depends_on = "etl-extract" # runs after etl-extract succeeds
[[cron.tasks]]
id = "etl-load"
schedule = ""
depends_on = "etl-transform"
Conditional Execution
Tasks can include conditions:
[[cron.tasks]]
id = "weekend-summary"
schedule = "0 10 * * SAT"
[cron.tasks.condition]
# Only run if there were messages in the past week
check = "message_count"
threshold = 10
period = "7d"
Timezone Support
All schedules support timezone specification:
[[cron.tasks]]
id = "standup-reminder"
schedule = "0 9 * * MON-FRI"
timezone = "America/New_York" # defaults to UTC if unset
[[cron.tasks]]
id = "asia-report"
schedule = "0 9 * * MON-FRI"
timezone = "Asia/Tokyo"
When no timezone is specified, schedules use UTC. Always set an explicit timezone for tasks that should run at local business hours.
Troubleshooting
| Problem | Solution |
|---|---|
| Task not running | Check enabled = true, verify schedule syntax with clawdesk cron run <id> --dry-run |
| Task runs at wrong time | Verify timezone setting, check system clock |
| Task always skipped | Check overlap policy — may be hitting overlap with long-running previous execution |
| Heartbeat stale alerts | Increase stale_threshold_secs or investigate slow provider responses |
| Task output not sent | Verify channel ID exists and is connected |