{"record":{"id":"762eb93ea294e73d","repo":"tinyhumansai/openhuman","slug":"at-least-one-of-expression-tz-command-or","errorCode":null,"errorMessage":"At least one of --expression, --tz, --command, or --name must be provided","messagePattern":"At least one of --expression, --tz, --command, or --name must be provided","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"src/openhuman/cron/ops.rs","lineNumber":84,"sourceCode":"        },\n    )\n}\n\n/// Update an existing cron job using the same rules as the legacy CLI, but without CLI wiring.\n///\n/// `expression` and `tz` are merged with the existing [`Schedule::Cron`] fields; the\n/// existing `active_hours` is always preserved as-is.  To set or clear `active_hours`\n/// directly, use the RPC path (`cron.update` with a full [`CronJobPatch`]).\npub fn update_cron_job(\n    config: &Config,\n    id: &str,\n    expression: Option<String>,\n    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,","sourceCodeStart":66,"sourceCodeEnd":102,"githubUrl":"https://github.com/tinyhumansai/openhuman/blob/749120085864ce16e0f273c7b86fac7740b39c5b/src/openhuman/cron/ops.rs#L66-L102","documentation":"update_cron_job refuses a no-op update: all four optional fields (expression, tz, command, name) are None, so there would be nothing to patch. The guard turns an empty patch into an explicit error instead of a silent non-change.","triggerScenarios":"CLI `openhuman cron update <id>` invoked without any of --expression/--tz/--command/--name; RPC callers building a partial update where every field is None; UI forms submitting although no field changed.","commonSituations":"A Save button that always calls update even with no diff; option parsers mapping missing flags to None; scripting loops over a diff set that turns out empty.","solutions":["Pass at least one flag: --expression, --tz, --command, or --name","If the intent was enable/disable, use the RPC path (`cron.update` with a full CronJobPatch carrying `enabled`) rather than this merge helper","In UI/script code, skip the update call when the changed-field set is empty"],"exampleFix":"# before\nopenhuman cron update morning-digest\n\n# after\nopenhuman cron update morning-digest --name \"daily-digest\"","handlingStrategy":"validation","validationCode":"let changes = [&expression, &tz, &command, &name].into_iter().flatten().count();\nif changes == 0 {\n    return Ok(job_unchanged); // nothing to do — skip the update call entirely\n}","typeGuard":null,"tryCatchPattern":"Catch the bail at the ops boundary and translate it into a 4xx 'empty patch' response for RPC callers instead of letting it surface as a store-level failure.","preventionTips":["Diff-then-update: compute the changed-field set first and only call update when non-empty","Disable Save buttons until at least one field differs","Document that the CLI update requires at least one flag"],"tags":["cron","cli","rpc","validation"],"backgroundTag":"missing-required-parameter","analyzedSha":"749120085864ce16e0f273c7b86fac7740b39c5b","analyzedAt":"2026-08-17T21:21:45.363Z","schemaVersion":2},"datasetVersion":"2026-08-21T18:17:14.833Z"}