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
- Fetch the job first (cron.get) and branch: only send expression/tz when the schedule is Cron
- 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)
- 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
- Check the schedule variant before offering expression/timezone edit fields in the UI
- Keep separate edit forms for cron vs one-shot vs interval jobs
- When converting schedule types, always go through the full-patch RPC, never the merge helper
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
- No future occurrence found within active hours after 100,000
- Invalid schedule: every_ms must be > 0
- Invalid schedule: 'at' must be in the future
- Not running in Tauri
- At least one of --expression, --tz, --command, or --name mus
AI-assisted analysis of tinyhumansai/openhuman@7491200858 (2026-08-17).
Data as JSON: /api/errors/13c703c43b35cb53.
Report an issue: GitHub.