tinyhumansai/openhuman · error

Cannot update expression/tz on a non-cron schedule

Error message

Cannot update expression/tz on a non-cron schedule

What it means

update_cron_job can only merge expression/tz into an existing Schedule::Cron — the merge preserves active_hours and whichever of expr/tz was not supplied. When the stored job's schedule is Schedule::At or Schedule::Every there is no cron expr/tz to merge into, so the update bails.

Source

Thrown at src/openhuman/cron/ops.rs:97

    tz: Option<String>,
    command: Option<String>,
    name: Option<String>,
) -> Result<CronJob> {
    if expression.is_none() && tz.is_none() && command.is_none() && name.is_none() {
        anyhow::bail!("At least one of --expression, --tz, --command, or --name must be provided");
    }

    // Merge expression/tz with the existing schedule so that
    // tz alone updates the timezone and expression alone preserves the timezone.
    let schedule = if expression.is_some() || tz.is_some() {
        let existing = get_job(config, id)?;
        let (existing_expr, existing_tz, existing_active) = match existing.schedule {
            Schedule::Cron {
                expr,
                tz: existing_tz,
                active_hours: existing_active,
            } => (expr, existing_tz, existing_active),
            _ => anyhow::bail!("Cannot update expression/tz on a non-cron schedule"),
        };
        Some(Schedule::Cron {
            expr: expression.unwrap_or(existing_expr),
            tz: tz.or(existing_tz),
            active_hours: existing_active,
        })
    } else {
        None
    };

    if let Some(ref cmd) = command {
        let security = SecurityPolicy::from_config(
            &config.autonomy,
            &config.workspace_dir,
            &config.action_dir,
        );
        if !security.is_command_allowed(cmd) {
            anyhow::bail!("Command blocked by security policy: {cmd}");

View on GitHub (pinned to 7491200858)

Solutions

  1. Fetch the job first (cron.get) and branch: only send expression/tz when the schedule is Cron
  2. To convert the schedule, replace it wholesale via the `cron.update` RPC with a full CronJobPatch whose schedule is a complete Schedule::Cron (the documented path for direct schedule control)
  3. Or delete and recreate the job with the cron schedule if history is irrelevant

Example fix

// before: merging into an `at`/`every` job fails
update_cron_job(&config, id, Some("*/5 * * * *".into()), None, None, None)?;

// after: full-patch RPC path replaces the schedule
let patch = CronJobPatch {
    schedule: Some(Schedule::Cron { expr: "*/5 * * * *".into(), tz: None, active_hours: None }),
    ..CronJobPatch::default()
};
update_job(&config, id, patch)?;
Defensive patterns

Strategy: validation

Validate before calling

let job = get_job(&config, id)?;
if !matches!(job.schedule, Schedule::Cron { .. }) {
    return Err(format!("job {id} is not cron-scheduled; use cron.update with a full patch"));
}
// safe to merge expression/tz now

Type guard

function isCronJob(job: CronJob): boolean {
  return job.schedule?.type === 'cron'; // discriminators: 'cron' | 'at' | 'every'
}

Prevention

When it happens

Trigger: Job was created as a one-shot (`at`) or interval (`every`) — e.g. via the once/every helpers — and a later `openhuman cron update <id> --expression ...` or --tz targets it; RPC update merging cron fields into a non-cron job.

Common situations: Promoting a one-shot reminder into a recurring job; copy-pasting an update command written for a cron job onto an every-job; a single edit form shown for all job types.

Related errors


AI-assisted analysis of tinyhumansai/openhuman@7491200858 (2026-08-17). Data as JSON: /api/errors/13c703c43b35cb53. Report an issue: GitHub.