Skip to content

11.1 Cron Job Configuration

Nikolay Vyahhi edited this page Feb 19, 2026 · 2 revisions

Cron Job Configuration

Relevant source files

The following files were used as context for generating this wiki page:

This page documents the configuration schema for scheduled jobs in ZeroClaw's cron scheduler system. For information about scheduler security enforcement and runtime behavior, see Scheduler Security. For information about the overall scheduler architecture and polling mechanism, see Scheduler.

Purpose and Scope

The cron scheduler supports two job types: shell jobs (execute commands) and agent jobs (invoke the agent with a prompt). Jobs are defined with a schedule (cron expression, fixed interval, or one-time execution), optional delivery configuration (announce results to a channel), and security constraints inherited from the global autonomy policy. This page covers:

  • Job type selection and configuration
  • Schedule syntax (cron, every, at)
  • Delivery modes and channel routing
  • Retry policies and timeout configuration
  • One-shot job lifecycle
  • Security validation applied at execution time

Job Types

The scheduler supports two execution modes defined by the JobType enum:

Type Description Configuration Fields
Shell Executes a shell command via sh -lc command (required)
Agent Invokes the agent turn cycle with a prompt prompt (required), model (optional), session_target (optional)

Sources: src/cron/scheduler.rs:8, src/cron/scheduler.rs:63-64

Shell Job Configuration

Shell jobs execute arbitrary commands in the workspace directory with a 120-second timeout. The command is passed to sh -lc to support shell features like pipes, environment variable expansion, and redirection.

Security enforcement:

  • Autonomy level must allow actions (can_act() returns true)
  • Command must pass allowlist validation
  • Path arguments are validated against forbidden directories
  • Rate limiting is enforced
  • Symlink escape detection prevents path traversal

Sources: src/cron/scheduler.rs:377-389, src/cron/scheduler.rs:391-467

Agent Job Configuration

Agent jobs invoke crate::agent::run() with a prefixed prompt format: [cron:{job_id} {name}] {prompt}. The agent executes a full turn cycle, including tool calls and history management.

Configuration options:

  • session_target: Controls agent session isolation
    • Main: Shares conversation history with interactive sessions
    • Isolated: Independent conversation context
  • model: Optional model override (defaults to configured provider model)
  • prompt: The user message sent to the agent

Performance warning: Agent jobs scheduled more frequently than every 5 minutes trigger a warning log because they incur LLM API costs on every execution.

Sources: src/cron/scheduler.rs:119-150, src/cron/scheduler.rs:213-238

Schedule Types

Three schedule formats are supported, represented by the Schedule enum:

graph TD
    Schedule["Schedule enum"]
    Cron["Cron { expr, tz }"]
    Every["Every { every_ms }"]
    At["At { at }"]
    
    Schedule --> Cron
    Schedule --> Every
    Schedule --> At
    
    Cron --> CronDesc["Standard cron expression<br/>with optional timezone"]
    Every --> EveryDesc["Fixed interval in milliseconds"]
    At --> AtDesc["One-time execution<br/>at specific timestamp"]
Loading

Sources: src/cron/scheduler.rs:8

Cron Expression Schedule

Standard 5-field cron syntax (minute hour day-of-month month day-of-week) with optional timezone support.

Example expressions:

  • "0 9 * * MON-FRI" - Every weekday at 9:00 AM
  • "*/15 * * * *" - Every 15 minutes
  • "0 0 1 * *" - First day of every month at midnight

Timezone support: The optional tz field accepts IANA timezone identifiers (e.g., "America/New_York", "Europe/London"). If omitted, UTC is used.

Sources: src/cron/scheduler.rs:217-230

Fixed Interval Schedule

The Every variant schedules jobs at fixed millisecond intervals. This is useful for periodic tasks that don't align with wall-clock boundaries.

Example:

  • every_ms = 300000 - Every 5 minutes
  • every_ms = 3600000 - Every hour

Sources: src/cron/scheduler.rs:218

One-time Execution Schedule

The At variant schedules a job to run once at a specific timestamp. When combined with delete_after_run = true, the job is automatically removed after successful execution or disabled after failure.

Example:

  • at = "2024-12-31T23:59:59Z" - Execute once on New Year's Eve

Lifecycle behavior:

  • Success: Job is deleted from the database
  • Failure: Job is disabled (enabled = false) and preserved for debugging

Sources: src/cron/scheduler.rs:181-211, src/cron/scheduler.rs:209-211

Delivery Configuration

The DeliveryConfig struct controls result announcement to channels after job execution.

Field Type Description
mode String Delivery mode; "announce" enables delivery, other values disable it
channel Option<String> Channel type: "telegram", "discord", "slack", "mattermost"
to Option<String> Target identifier (user ID, channel ID, thread identifier)
best_effort bool If true, delivery failure logs a warning but doesn't fail the job

Sources: src/cron/scheduler.rs:8, src/cron/scheduler.rs:240-317

Delivery Modes

Only mode = "announce" triggers delivery. Any other value (or empty string) disables result announcement.

Channel routing:

  • Telegram: to is the chat ID (e.g., "123456789")
  • Discord: to is the channel ID (e.g., "987654321098765432")
  • Slack: to is the channel ID (e.g., "C01ABCD1234")
  • Mattermost: to is channel_id or channel_id:root_id for threaded replies

Best-effort delivery: When best_effort = true, delivery failures are logged as warnings but don't mark the job as failed. When false (default), delivery failures cause the job to fail and trigger retry logic.

Sources: src/cron/scheduler.rs:240-244, src/cron/scheduler.rs:162-169

Channel Implementation Requirements

Delivery requires the corresponding channel to be configured in config.toml under [channels_config]. The scheduler instantiates channel objects on-demand:

graph LR
    Job["Cron Job Execution"]
    Delivery["deliver_if_configured()"]
    TelegramCh["TelegramChannel::new()"]
    DiscordCh["DiscordChannel::new()"]
    SlackCh["SlackChannel::new()"]
    MattermostCh["MattermostChannel::new()"]
    
    Job --> Delivery
    Delivery -->|"channel = telegram"| TelegramCh
    Delivery -->|"channel = discord"| DiscordCh
    Delivery -->|"channel = slack"| SlackCh
    Delivery -->|"channel = mattermost"| MattermostCh
    
    TelegramCh --> Send["channel.send()"]
    DiscordCh --> Send
    SlackCh --> Send
    MattermostCh --> Send
Loading

Sources: src/cron/scheduler.rs:240-317

Job Execution Flow

The scheduler executes jobs with retry logic, security validation, and result persistence:

sequenceDiagram
    participant Scheduler as "run() loop"
    participant DB as "due_jobs()"
    participant Security as "SecurityPolicy"
    participant Executor as "execute_job_with_retry()"
    participant Job as "Shell/Agent Job"
    participant Delivery as "deliver_if_configured()"
    participant Persist as "record_run() / reschedule_after_run()"
    
    Scheduler->>DB: Query jobs with next_run <= now
    DB-->>Scheduler: Vec<CronJob>
    
    loop For each due job (max_concurrent)
        Scheduler->>Executor: execute_and_persist_job()
        Executor->>Security: can_act() / record_action()
        Security-->>Executor: Allowed/Denied
        
        alt Security Denied
            Executor-->>Scheduler: (job_id, false)
        else Security Approved
            loop Retry logic (0..=retries)
                Executor->>Job: run_job_command() or run_agent_job()
                Job-->>Executor: (success, output)
                
                alt Success or Deterministic Failure
                    Note over Executor: Break retry loop
                else Transient Failure
                    Note over Executor: Exponential backoff + jitter
                end
            end
            
            Executor->>Delivery: Announce result to channel
            alt Delivery Enabled
                Delivery-->>Executor: Success/Failure
            end
            
            Executor->>Persist: record_run() + reschedule_after_run()
            Persist-->>Executor: Updated job metadata
            Executor-->>Scheduler: (job_id, success)
        end
    end
Loading

Sources: src/cron/scheduler.rs:21-44, src/cron/scheduler.rs:52-85, src/cron/scheduler.rs:87-101, src/cron/scheduler.rs:103-117

Security Validation Sequence

Before executing any job, the scheduler enforces security policies in this order:

  1. Autonomy Check: security.can_act() must return true (blocks ReadOnly mode)
  2. Rate Limit Check: security.is_rate_limited() must return false
  3. Command Allowlist: For shell jobs, security.is_command_allowed(command) validates against allowed_commands
  4. Path Validation: For shell jobs, forbidden_path_argument() blocks access to system directories
  5. Action Recording: security.record_action() increments the action counter for rate limiting

Deterministic policy violations (autonomy, rate limit, command allowlist, forbidden paths) are not retried because they will always fail.

Sources: src/cron/scheduler.rs:397-433, src/cron/scheduler.rs:319-375

Retry Policies

Job execution uses exponential backoff with jitter for transient failures:

Configuration Default Description
scheduler_retries 3 Maximum retry attempts (from config.reliability.scheduler_retries)
provider_backoff_ms 1000 Initial backoff delay (from config.reliability.provider_backoff_ms)
Backoff multiplier Each retry doubles the delay (capped at 30 seconds)
Jitter 0-250ms Random jitter added to prevent thundering herd

Retry behavior:

  • Transient failures (command timeout, network errors) are retried up to scheduler_retries times
  • Deterministic failures (security policy violations) are not retried
  • Backoff calculation: delay = min(backoff_ms * 2^attempt + jitter, 30000)

Sources: src/cron/scheduler.rs:52-85, src/cron/scheduler.rs:58-59, src/cron/scheduler.rs:77-81

Shell Job Timeout

Shell jobs have a hardcoded 120-second execution timeout. If a command exceeds this limit, it is killed and the job is marked as failed.

Sources: src/cron/scheduler.rs:19, src/cron/scheduler.rs:449

Configuration Examples

Example 1: Daily Backup Shell Job

[[scheduler.jobs]]
id = "daily-backup"
name = "Database Backup"
schedule.cron = "0 2 * * *"
schedule.tz = "America/New_York"
command = "pg_dump mydb | gzip > /backups/mydb-$(date +%Y%m%d).sql.gz"
enabled = true
delete_after_run = false

[scheduler.jobs.delivery]
mode = "announce"
channel = "slack"
to = "C01BACKUP"
best_effort = false

Example 2: Agent Job with Model Override

[[scheduler.jobs]]
id = "weekly-summary"
name = "Generate Weekly Report"
schedule.cron = "0 9 * * MON"
prompt = "Generate a summary of last week's activity from memory and email it to the team."
model = "openai/gpt-4"
session_target = "isolated"
enabled = true

[scheduler.jobs.delivery]
mode = "announce"
channel = "mattermost"
to = "team-updates-channel-id"
best_effort = true

Example 3: One-shot Notification

[[scheduler.jobs]]
id = "release-reminder"
name = "Release Day Reminder"
schedule.at = "2024-12-15T09:00:00Z"
prompt = "Remind the team that today is release day. Check the release checklist and report any blockers."
session_target = "main"
enabled = true
delete_after_run = true

[scheduler.jobs.delivery]
mode = "announce"
channel = "telegram"
to = "123456789"
best_effort = false

Example 4: High-frequency Health Check

[[scheduler.jobs]]
id = "health-check"
name = "Service Health Monitor"
schedule.every_ms = 300000  # Every 5 minutes
command = "curl -f http://localhost:8080/health || echo 'Service down!'"
enabled = true

[scheduler.jobs.delivery]
mode = "announce"
channel = "discord"
to = "987654321098765432"
best_effort = true

Sources: src/cron/scheduler.rs:8 (CronJob struct definition)

Job Metadata and State Tracking

The scheduler maintains execution state for each job:

Field Type Description
next_run DateTime<Utc> Calculated next execution time (updated by reschedule_after_run)
last_run Option<DateTime<Utc>> Timestamp of most recent execution
last_status Option<String> "ok" or "error" from last run
last_output Option<String> Captured stdout/stderr or agent response

State transitions:

  1. Job Created: next_run is calculated from schedule + creation time
  2. Job Executed: last_run, last_status, and last_output are updated
  3. Rescheduled: next_run is recalculated based on schedule type
  4. One-shot Success: Job is deleted from database
  5. One-shot Failure: Job is disabled (enabled = false)

Sources: src/cron/scheduler.rs:152-207

Agent Job Session Targets

Agent jobs support two session isolation modes via the SessionTarget enum:

Target Description Use Case
Main Shares conversation history with interactive sessions Continuity with user conversations
Isolated Independent conversation context per job execution Stateless scheduled tasks

The session_target field is only relevant for agent jobs; it has no effect on shell jobs.

Sources: src/cron/scheduler.rs:8, src/cron/scheduler.rs:125-137

Command and Path Security

Shell jobs undergo multi-layered path validation to prevent privilege escalation and data exfiltration:

Forbidden Path Detection

The forbidden_path_argument() function parses shell commands and blocks access to system directories:

Forbidden paths (enforced by SecurityPolicy::is_path_allowed()):

  • /etc, /sys, /proc, /dev, /boot, /root, /var/log, /var/spool, /tmp, /var/tmp, /usr/bin, /usr/sbin, /sbin, /bin

Parsing logic:

  1. Split command on &&, ||, ;, |, newlines
  2. For each segment, skip leading environment variable assignments (KEY=value)
  3. Skip the executable token (first non-assignment word)
  4. For remaining tokens, strip quotes and check if they look like paths
  5. Validate path candidates against SecurityPolicy::is_path_allowed()

Path detection heuristics:

  • Starts with /, ./, ../, ~/
  • Contains / anywhere in the token
  • Skips flags (starts with -) and URLs (contains ://)

Sources: src/cron/scheduler.rs:319-375, src/cron/scheduler.rs:421-426

Command Allowlist Validation

The allowed_commands configuration (from config.autonomy.allowed_commands) is enforced by SecurityPolicy::is_command_allowed(). Only the first token (executable name) is validated; arguments are not checked against the allowlist.

Example allowlist:

[autonomy]
allowed_commands = ["git", "npm", "cargo", "curl", "wget"]

Sources: src/cron/scheduler.rs:411-419

Warning: High-frequency Agent Jobs

The scheduler emits a warning when agent jobs are scheduled more frequently than every 5 minutes. This is a cost-protection mechanism because each agent job invokes an LLM API call, which incurs per-token charges.

Detection logic:

  • For Every schedules: every_ms < 5 * 60 * 1000
  • For Cron schedules: Calculate time delta between consecutive runs

Warning message:

"Cron agent job '{job_id}' is scheduled more frequently than every 5 minutes"

Sources: src/cron/scheduler.rs:213-238

Implementation References

Core scheduler loop: src/cron/scheduler.rs:21-44

Job execution with retry: src/cron/scheduler.rs:52-85

Shell command execution: src/cron/scheduler.rs:391-467

Agent job execution: src/cron/scheduler.rs:119-150

Result delivery: src/cron/scheduler.rs:240-317

Security validation: src/cron/scheduler.rs:397-433

Path argument scanning: src/cron/scheduler.rs:319-375

One-shot lifecycle: src/cron/scheduler.rs:181-211


Clone this wiki locally