shareAI-lab/learn-claude-code · warning · ValueError

expected a JSON list

Error message

expected a JSON list

What it means

Raised by load_durable_jobs() in s12_cron_scheduler/code.py:378 when the durable jobs file (.scheduled_tasks.json in the workspace) parses as JSON but its top level is not a list. CronJob records are stored as a JSON array, one object per job, so an object at the top level (e.g. a single job, or a dict keyed by job id) is a schema violation. The error is caught together with OSError and JSONDecodeError, logged as 'could not load .scheduled_tasks.json: expected a JSON list', and loading aborts — the scheduler then simply has no durable jobs.

Source

Thrown at s12_cron_scheduler/code.py:378

            if job.durable
        ]
        temporary = DURABLE_PATH.with_name(
            f"{DURABLE_PATH.name}.{os.getpid()}.{threading.get_ident()}.tmp"
        )
        try:
            temporary.write_text(json.dumps(payload, indent=2))
            os.replace(temporary, DURABLE_PATH)
        finally:
            temporary.unlink(missing_ok=True)


def load_durable_jobs():
    if not DURABLE_PATH.exists():
        return
    try:
        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}")

View on GitHub (pinned to 985456f4ad)

Solutions

  1. Restore the array shape: the file must be a JSON list of CronJob objects like [{"id": "cron_…", "cron": "…", "prompt": "…", ...}].
  2. If you have a dict-keyed export, convert it: json.dump(list(d.values()), f).
  3. Prefer recreating jobs through schedule_job() (which persists correctly) instead of editing the file, and keep a backup before manual edits.

Example fix

# before: .scheduled_tasks.json
{"cron_ab12cd34": {"id": "cron_ab12cd34", "cron": "*/5 * * * *", "prompt": "hi"}}

# after
[{"id": "cron_ab12cd34", "cron": "*/5 * * * *", "prompt": "hi", "recurring": true, "durable": true, "pending_delivery": false}]
Defensive patterns

Strategy: validation

Validate before calling

import json
from pathlib import Path

def durable_file_is_valid(path: Path) -> bool:
    try:
        return isinstance(json.loads(path.read_text()), list)
    except (OSError, json.JSONDecodeError):
        return False

Type guard

def is_job_list(payload) -> bool:
    return isinstance(payload, list) and all(isinstance(j, dict) for j in payload)

Try / catch

try:
    payload = json.loads(path.read_text())
    assert isinstance(payload, list)
except (ValueError, AssertionError):
    backup_and_recreate(path)  # the loader only logs, so repair before restart

Prevention

When it happens

Trigger: Hand-editing .scheduled_tasks.json into {"cron_ab12cd34": {...}} or a single {...} job object; an external tool writing a dict-keyed format; a partial/interrupted write leaving valid JSON of the wrong shape (unlikely, since persistence uses atomic os.replace, so this is mainly manual edits or foreign writers).

Common situations: Users merging or 'cleaning up' the file by hand; a different version of the tool once persisted a dict; scripts that append to the file with a different structure.

Related errors


AI-assisted analysis of shareAI-lab/learn-claude-code@985456f4ad (2026-08-14). Data as JSON: /api/errors/c26234794f08890b. Report an issue: GitHub.