shareAI-lab/learn-claude-code · warning · ValueError
prompt cannot be empty
Error message
prompt cannot be empty
What it means
Raised during load_durable_jobs() in s12_cron_scheduler/code.py:394 when a saved job's prompt field is empty after stripping. Every cron job exists to deliver a prompt, so a blank prompt makes the job a no-op; on reload each job is validated in sequence (cron expression, id prefix, then prompt) and this failure is caught per-job, logged as 'skipped invalid saved job', and the rest of the file loads normally.
Source
Thrown at s12_cron_scheduler/code.py:394
payload = json.loads(DURABLE_PATH.read_text())
if not isinstance(payload, list):
raise ValueError("expected a JSON list")
except (OSError, json.JSONDecodeError, ValueError) as error:
print(f" [cron] could not load {DURABLE_PATH.name}: {error}")
return
loaded = 0
with cron_lock:
for item in payload:
try:
job = CronJob(**item)
error = validate_cron(job.cron)
if error:
raise ValueError(error)
if not job.id.startswith("cron_"):
raise ValueError("invalid job ID")
if not job.prompt.strip():
raise ValueError("prompt cannot be empty")
except (TypeError, ValueError) as error:
print(f" [cron] skipped invalid saved job: {error}")
continue
scheduled_jobs[job.id] = job
if job.pending_delivery:
cron_queue.append(job)
loaded += 1
if loaded:
print(f" [cron] loaded {loaded} durable job(s)")
def new_cron_id() -> str:
for _ in range(100):
job_id = f"cron_{secrets.token_hex(4)}"
if job_id not in scheduled_jobs:
return job_id
raise RuntimeError("Could not allocate a cron job ID")
View on GitHub (pinned to 985456f4ad)
Solutions
- Give the job a non-empty prompt (any non-blank string) or delete the entry entirely rather than keeping a hollow job.
- Create prompts only through schedule_job(), which rejects blank prompts at creation time ('Prompt cannot be empty').
- After editing, rerun and check the '[cron] loaded N durable job(s)' line to confirm all intended jobs survived.
Example fix
# before
{"id": "cron_1a2b3c4d", "cron": "0 3 * * *", "prompt": ""}
# after
{"id": "cron_1a2b3c4d", "cron": "0 3 * * *", "prompt": "Summarize yesterday's commits"} Defensive patterns
Strategy: validation
Validate before calling
def job_is_loadable(job: dict) -> bool:
return isinstance(job.get('prompt'), str) and bool(job['prompt'].strip()) Type guard
def has_prompt(job) -> bool:
return isinstance(job, dict) and isinstance(job.get('prompt'), str) and bool(job['prompt'].strip()) Try / catch
for job in payload:
if not has_prompt(job):
log.warning('dropping promptless job %s', job.get('id'))
continue
register(job) Prevention
- Create jobs only through schedule_job(), which rejects blank prompts at creation.
- Delete unwanted jobs entirely instead of blanking their prompt.
- Check the loaded-jobs count after every manual file edit.
When it happens
Trigger: A .scheduled_tasks.json entry with "prompt": "" or " "; a prompt key missing entirely (TypeError, also skipped); hand-edited entries where the prompt text was deleted; a foreign writer that persisted the prompt under a different key name.
Common situations: Manual pruning of the durable file that empties prompts; migration scripts that drop the field; testing edits that replace the prompt with a placeholder whitespace string.
Related errors
- invalid job ID
- Task {task_id} not found
- Task {task_id} not found
- expected a JSON list
- Max 20 todos allowed
AI-assisted analysis of shareAI-lab/learn-claude-code@985456f4ad (2026-08-14).
Data as JSON: /api/errors/964705d10fb93f29.
Report an issue: GitHub.