How the OpenHuman Cron Job System Schedules and Delivers Background Tasks
The OpenHuman cron job system persists job definitions in SQLite, evaluates schedules to compute next run times, executes due jobs via shell commands or the QuickJS runtime, and delivers outputs through a domain event bus that routes to configured channels or webhooks.
The OpenHuman cron job system provides a native background-task subsystem that runs inside the core side-car process of the tinyhumansai/openhuman repository. It combines persistent storage with async scheduling to execute both shell commands and AI agent tasks, using a domain-event architecture to deliver results to chat channels, webhooks, or desktop notifications.
System Architecture and Data Flow
The cron implementation follows an eight-step pipeline orchestrated by the async entry point scheduler::run in src/openhuman/cron/scheduler.rs. This function starts on SystemStartup (line 27) and runs until the core process exits.
- Job definitions are stored in the SQLite workspace via
src/openhuman/cron/store.rs. - Schedule evaluation converts cron expressions or intervals into concrete
next_runtimestamps usingsrc/openhuman/cron/schedule.rs. - Due-job detection queries
store::due_jobsfor jobs wherenext_run≤ now. - Execution handles shell commands via
std::process::Commandor agent jobs via the QuickJS runtime (quickjs::Engine::run_agent). - Result recording writes a
CronRunentry viastore::record_run. - Rescheduling recalculates
next_runviastore::reschedule_after_runor deletes one-shot jobs. - Delivery publishing emits a
DomainEvent::CronDeliveryRequestedcontainingDeliveryConfigmetadata. - Event routing consumes the event in
src/openhuman/cron/bus.rsand forwards output to the target channel.
Storing and Retrieving Cron Jobs
All job state lives in a SQLite table managed by src/openhuman/cron/store.rs. The module provides CRUD helpers such as add_job, add_shell_job, add_agent_job, list_jobs, and remove_job.
Each row maps to a CronJob struct defined in src/openhuman/cron/types.rs, containing:
- A unique job ID
- A
Schedulevariant (cron expression, fixed interval, or one-shot) - Command strings or agent prompts
- A
DeliveryConfigspecifying output destination and mode
The scheduler polls for work using store::due_jobs, which executes SELECT * FROM cron_jobs WHERE next_run ≤ :now to identify tasks ready for execution.
Schedule Parsing and Validation
Before a job can run, its schedule must be normalized to a concrete timestamp. The src/openhuman/cron/schedule.rs module handles this via next_run_for_schedule, validate_schedule, and normalize_expression.
These functions accept human-readable inputs—standard cron syntax (e.g., */5 * * * *), fixed durations, or absolute timestamps—and return a DateTime<Utc> stored in the next_run column. This approach allows the main loop to perform simple timestamp comparisons rather than parsing cron syntax on every tick.
The Execution Loop and Job Types
The core async loop resides in src/openhuman/cron/scheduler.rs. When execute_job_now is invoked, it distinguishes between two execution modes:
- Shell jobs: Spawn a subprocess using
std::process::Commandwithsh, capturing stdout and stderr. - Agent jobs: Invoke the QuickJS skill runtime (
openhuman::cron::ops::add_agent_job), which may call LLM prompts and return generated content.
Both variants return a (bool success, String output) tuple that feeds into the recording and delivery phases.
Recording Results and Rescheduling
After execution, store::record_run inserts a row into the cron_runs table and updates the parent job's last_* fields to track history. The system then calls store::reschedule_after_run to determine the job's next state:
- For repeating jobs, it recomputes
next_runbased on the original schedule. - For one-shot jobs or when
delete_after_runis set, it removes the job from the database.
Delivering Outputs via the Event Bus
Output delivery decouples execution from transport via the domain event bus. The scheduler::deliver_job function constructs a DomainEvent::CronDeliveryRequested containing:
- The job output string
- A
DeliveryConfigstruct (mode, channel,toaddress,best_effortflag) - Metadata linking back to the original
CronJob
The CronDeliverySubscriber in src/openhuman/cron/bus.rs listens for these events and routes messages to the appropriate driver—whether that's a chat channel handler, a webhook dispatcher, or the announce broadcast for desktop UI notifications.
Practical Example: Creating a Cron Job
The following Rust code demonstrates adding a shell job and forcing immediate execution:
use openhuman::cron::{self, Schedule};
// Create a repeating shell job that echoes every 5 minutes
let cfg = &config; // core Config instance
let job = cron::add_shell_job(
cfg,
"my-hello-job",
Schedule::Cron { expr: "*/5 * * * *".into() },
"echo hello",
)?;
// Force-run immediately for testing
let (success, output) = cron::scheduler::execute_job_now(cfg, &job).await;
println!("Run success={}, output={}", success, output);
// On the next scheduler tick, due_jobs will detect this task,
// execute it, and publish CronDeliveryRequested for the bus subscriber.
Additional startup-time seeding occurs in src/openhuman/cron/seed.rs, which emits DomainEvent::ProactiveMessageRequested for agent-initiated conversations.
Summary
- Persistence: Jobs live in SQLite (
store.rs) with CRUD operations for shell and agent tasks. - Scheduling: Cron expressions and intervals are parsed to concrete timestamps (
schedule.rs), queried viadue_jobs. - Execution: An async loop (
scheduler.rs) runs shell commands or QuickJS agents and captures output. - Rescheduling: Completed jobs are either rescheduled or deleted based on their configuration.
- Delivery: Results travel through a domain event bus (
bus.rs) usingCronDeliveryRequestedevents, routing to channels or webhooks defined inDeliveryConfig.
Frequently Asked Questions
How does OpenHuman persist cron job definitions?
OpenHuman stores cron jobs in an SQLite table accessed via src/openhuman/cron/store.rs. Helper functions like add_shell_job and add_agent_job write CronJob structs containing schedules, commands, and delivery configurations to the database, where the next_run column indexes upcoming executions.
What types of tasks can the OpenHuman cron system execute?
The system supports two task types defined in src/openhuman/cron/scheduler.rs: shell jobs that spawn subprocesses via std::process::Command, and agent jobs that execute JavaScript/TypeScript skills in the QuickJS runtime (quickjs::Engine::run_agent), enabling LLM-based workflows.
How are task outputs delivered to users or systems?
After execution, scheduler::deliver_job publishes a DomainEvent::CronDeliveryRequested event containing the output and a DeliveryConfig. The CronDeliverySubscriber in src/openhuman/cron/bus.rs consumes this event and routes the message to chat channels, webhooks, or desktop announcements based on the configuration's mode and channel fields.
When does the scheduler evaluate and run due jobs?
The scheduler evaluates jobs continuously within the async scheduler::run loop, which starts on SystemStartup in src/openhuman/cron/scheduler.rs (line 27). On each iteration, it queries store::due_jobs for records where next_run is less than or equal to the current UTC timestamp, executing all matches before sleeping until the next cycle.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →