{"record":{"id":"13c703c43b35cb53","repo":"tinyhumansai/openhuman","slug":"cannot-update-expression-tz-on-a-non-cron-schedule","errorCode":null,"errorMessage":"Cannot update expression/tz on a non-cron schedule","messagePattern":"Cannot update expression/tz on a non-cron schedule","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"src/openhuman/cron/ops.rs","lineNumber":97,"sourceCode":"    tz: Option<String>,\n    command: Option<String>,\n    name: Option<String>,\n) -> Result<CronJob> {\n    if expression.is_none() && tz.is_none() && command.is_none() && name.is_none() {\n        anyhow::bail!(\"At least one of --expression, --tz, --command, or --name must be provided\");\n    }\n\n    // Merge expression/tz with the existing schedule so that\n    // tz alone updates the timezone and expression alone preserves the timezone.\n    let schedule = if expression.is_some() || tz.is_some() {\n        let existing = get_job(config, id)?;\n        let (existing_expr, existing_tz, existing_active) = match existing.schedule {\n            Schedule::Cron {\n                expr,\n                tz: existing_tz,\n                active_hours: existing_active,\n            } => (expr, existing_tz, existing_active),\n            _ => anyhow::bail!(\"Cannot update expression/tz on a non-cron schedule\"),\n        };\n        Some(Schedule::Cron {\n            expr: expression.unwrap_or(existing_expr),\n            tz: tz.or(existing_tz),\n            active_hours: existing_active,\n        })\n    } else {\n        None\n    };\n\n    if let Some(ref cmd) = command {\n        let security = SecurityPolicy::from_config(\n            &config.autonomy,\n            &config.workspace_dir,\n            &config.action_dir,\n        );\n        if !security.is_command_allowed(cmd) {\n            anyhow::bail!(\"Command blocked by security policy: {cmd}\");","sourceCodeStart":79,"sourceCodeEnd":115,"githubUrl":"https://github.com/tinyhumansai/openhuman/blob/749120085864ce16e0f273c7b86fac7740b39c5b/src/openhuman/cron/ops.rs#L79-L115","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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"],"exampleFix":"// before: merging into an `at`/`every` job fails\nupdate_cron_job(&config, id, Some(\"*/5 * * * *\".into()), None, None, None)?;\n\n// after: full-patch RPC path replaces the schedule\nlet patch = CronJobPatch {\n    schedule: Some(Schedule::Cron { expr: \"*/5 * * * *\".into(), tz: None, active_hours: None }),\n    ..CronJobPatch::default()\n};\nupdate_job(&config, id, patch)?;","handlingStrategy":"validation","validationCode":"let job = get_job(&config, id)?;\nif !matches!(job.schedule, Schedule::Cron { .. }) {\n    return Err(format!(\"job {id} is not cron-scheduled; use cron.update with a full patch\"));\n}\n// safe to merge expression/tz now","typeGuard":"function isCronJob(job: CronJob): boolean {\n  return job.schedule?.type === 'cron'; // discriminators: 'cron' | 'at' | 'every'\n}","tryCatchPattern":null,"preventionTips":["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"],"tags":["cron","schedule","update"],"backgroundTag":"schedule-type-mismatch","analyzedSha":"749120085864ce16e0f273c7b86fac7740b39c5b","analyzedAt":"2026-08-17T21:21:45.363Z","schemaVersion":2},"datasetVersion":"2026-08-21T23:17:16.201Z"}