{"record":{"id":"361480125de40509","repo":"zeroclaw-labs/zeroclaw","slug":"declarative-cron-job-has-empty-id","errorCode":null,"errorMessage":"Declarative cron job has empty id","messagePattern":"Declarative cron job has empty id","errorType":"validation","errorClass":"anyhow::Error","httpStatus":null,"severity":"error","filePath":"crates/zeroclaw-runtime/src/cron/store.rs","lineNumber":1398,"sourceCode":"                })?;\n\n                ::zeroclaw_log::record!(\n                    INFO,\n                    ::zeroclaw_log::Event::new(module_path!(), ::zeroclaw_log::Action::Note)\n                        .with_attrs(::serde_json::json!({\"job_id\": id})),\n                    \"Inserted declarative cron job from config\"\n                );\n            }\n        }\n\n        Ok(())\n    })\n}\n\n/// Validate a declarative cron job definition.\nfn validate_decl(id: &str, decl: &zeroclaw_config::schema::CronJobDecl) -> Result<()> {\n    if id.trim().is_empty() {\n        anyhow::bail!(\"Declarative cron job has empty id\");\n    }\n\n    match decl.job_type.to_lowercase().as_str() {\n        \"shell\" => {\n            if decl.command.as_deref().is_none_or(|c| c.trim().is_empty()) {\n                anyhow::bail!(\n                    \"Declarative cron job '{id}': shell job requires a non-empty 'command'\"\n                );\n            }\n        }\n        \"agent\" => {\n            if decl.prompt.as_deref().is_none_or(|p| p.trim().is_empty()) {\n                anyhow::bail!(\n                    \"Declarative cron job '{id}': agent job requires a non-empty 'prompt'\"\n                );\n            }\n            if decl.shell_output_format != zeroclaw_config::schema::CronShellOutputFormat::default()\n            {","sourceCodeStart":1380,"sourceCodeEnd":1416,"githubUrl":"https://github.com/zeroclaw-labs/zeroclaw/blob/88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc/crates/zeroclaw-runtime/src/cron/store.rs#L1380-L1416","documentation":"Thrown by validate_decl while sync_declarative_jobs copies declarative cron jobs from config into the SQLite cron store. The map key (a [cron.<id>] table key in zeroclaw.toml) becomes the job's permanent id used by the scheduler, the cron_jobs.id column, and CLI/API addressing, so an id that is empty or only whitespace cannot be addressed and validation aborts before any DB write. Validation runs for every entry before the sync touches the database.","triggerScenarios":"Daemon or scheduler startup (or the gateway API path) calls sync_declarative_jobs with a decls map containing a key of \"\" or only whitespace. In TOML this is a table like [cron.\"\"] or [cron.\" \"]; programmatically it is config.cron.insert(String::new(), decl) or a config generator emitting a blank id.","commonSituations":"Hand-editing zeroclaw.toml and leaving an empty table header after renaming a job; templating that renders an unset variable as the id (for example [cron.\"${JOB_NAME}\"]); copy-pasting a job block and deleting the id line.","solutions":["Open zeroclaw.toml and give the offending [cron.<id>] table a non-empty, unique id such as [cron.daily-backup]; search for [cron.\"\"] or whitespace-only keys","If config is generated, assert every emitted id is non-empty before writing the file","Restart the daemon or re-run zeroclaw cron list to confirm the job now syncs"],"exampleFix":"# zeroclaw.toml — before\n[cron.\"\"]\njob_type = \"shell\"\nschedule = { kind = \"cron\", expression = \"0 3 * * *\" }\ncommand = \"backup.sh\"\n\n# after\n[cron.daily-backup]\njob_type = \"shell\"\nschedule = { kind = \"cron\", expression = \"0 3 * * *\" }\ncommand = \"backup.sh\"","handlingStrategy":"validation","validationCode":"// run before syncing declarative cron jobs into the store\nfor id in decls.keys() {\n    if id.trim().is_empty() {\n        anyhow::bail!(\"zeroclaw.toml: a [cron.<id>] table key is empty or whitespace\");\n    }\n}\nzeroclaw_runtime::cron::sync_declarative_jobs(&config, &decls)?;","typeGuard":"fn cron_ids_are_addressable(decls: &std::collections::HashMap<String, zeroclaw_config::schema::CronJobDecl>) -> bool {\n    decls.keys().all(|id| !id.trim().is_empty())\n}","tryCatchPattern":"match zeroclaw_runtime::cron::sync_declarative_jobs(&config, &decls) {\n    Err(err) if err.to_string().contains(\"empty id\") => {\n        eprintln!(\"config error: a [cron.<id>] table has a blank id — fix zeroclaw.toml\");\n        std::process::exit(2);\n    }\n    other => other?,\n}","preventionTips":["Lint generated config: assert cron ids are non-empty and unique before writing the file","Use descriptive kebab-case ids (daily-backup) so blank or placeholder keys stand out in review","Run the daemon's config validation after every zeroclaw.toml edit instead of discovering errors at startup"],"tags":["cron","config-validation","toml"],"backgroundTag":"config-validation-failed","analyzedSha":"88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc","analyzedAt":"2026-08-23T01:07:41.857Z","schemaVersion":2},"datasetVersion":"2026-08-23T08:06:27.607Z"}