Skip to content

One-shot at cron jobs are disabled before execution, always skipped as "disabled" #91775

Description

@A1fred-AI

One-shot at cron jobs are disabled before execution, always skipped as "disabled"

Summary

One-shot cron jobs (schedule.kind: "at") are stored with enabled: false despite the cron add API returning enabled: true. When the scheduler fires the job at the scheduled time, it finds the job disabled and skips it with error "disabled". The deleteAfterRun cleanup never triggers because the run was skipped rather than completed, leaving zombie disabled jobs in the cron_jobs table.

This affects all one-shot jobs regardless of sessionTarget or payload.kind. Recurring jobs (schedule.kind: "cron" and "every") are unaffected.

Environment

  • OpenClaw: 2026.6.x (current main), macOS, gateway mode local
  • Runtime: Node 22.22.2, arm64
  • Storage: SQLite (~/.openclaw/state/openclaw.sqlite)

Reproduction

  1. Create a one-shot cron job via the API or CLI:
{
  "name": "test-wake-30s",
  "schedule": { "kind": "at", "at": "2026-06-09T22:38:50Z" },
  "sessionTarget": "main",
  "payload": { "kind": "systemEvent", "text": "Test wake: 30 second one-shot." },
  "delivery": { "mode": "none" },
  "enabled": true,
  "deleteAfterRun": true
}
  1. cron add returns enabled: true in the API response.

  2. Immediately query cron_jobs table:

    SELECT enabled, json_extract(job_json, '$.enabled') FROM cron_jobs WHERE name = 'test-wake-30s';
    -- Result: enabled=1, json_enabled=1  ✅ correct at this point
  3. Wait for the scheduled time to pass.

  4. Query again:

    SELECT enabled, last_run_status, last_error FROM cron_jobs WHERE name = 'test-wake-30s';
    -- Result: enabled=0, last_run_status='skipped', last_error='disabled'  ❌

The job fired at exactly the scheduled time (last_run_at_ms matches) but was skipped because it had been flipped to enabled: false.

Evidence

Run log

{
  "jobId": "80d302d1-71b8-4e76-8681-08a21cdf710a",
  "status": "skipped",
  "error": "disabled",
  "sessionKey": "agent:main:cron:80d302d1-71b8-4e76-8681-08a21cdf710a:run:1781040420024",
  "runAtMs": 1781040420024,
  "durationMs": 6
}

Duration of 6ms — the skip is immediate, no actual execution attempted.

Stored job JSON

{
  "id": "80d302d1-71b8-4e76-8681-08a21cdf710a",
  "enabled": false,   // ← was created with enabled: true
  "deleteAfterRun": true,
  "schedule": { "kind": "at", "at": "2026-06-09T21:27:00.000Z" },
  "sessionTarget": "main",
  "payload": { "kind": "systemEvent", "text": "..." }
}

Scale of impact

Across 3 long-run sprint sessions, 12 one-shot wake jobs were created. Every single one was stored as enabled: false and skipped as disabled. Zero one-shot jobs have ever succeeded on this installation. Recurring jobs (cron, every) work correctly.

-- Every one-shot job in our installation — all skipped as disabled
SELECT count(*) FROM cron_jobs 
WHERE schedule_kind = 'at' AND last_run_status = 'ok';
-- Result: 0

-- Recurring jobs — all work fine  
SELECT count(*) FROM cron_jobs 
WHERE schedule_kind IN ('cron', 'every') AND last_run_status = 'ok';
-- Result: 10+

Root Cause (suspected)

Looking at src/cron/service/timer.ts lines 1416+, the one-shot execution path appears to disable the job as part of its deleteAfterRun lifecycle before running the actual execution. The execution then finds the job already disabled and skips it.

The likely sequence:

  1. Scheduler timer fires, job is due
  2. One-shot cleanup disables the job (enabled: false) as the first step of deleteAfterRun
  3. Execution checks enabled, finds false, returns skipped: disabled
  4. Because the run was skipped (not ok), the deletion step doesn't execute
  5. Result: zombie disabled job persists in the table

This is a different issue from #67500 (which was about delivery stalls to idle sessions). #67500's cited fix commits (355c92d69b38 and 7e52223d3275) are unrelated to this lifecycle — they address config loading and image understanding registration respectively.

Workaround

None found for one-shot jobs. Affected users can work around the limitation by using recurring jobs (schedule.kind: "cron" or "every") and disabling them after the first successful run, or by using sessionTarget: "isolated" with payload.kind: "agentTurn" which uses push-based completion events rather than scheduled wakes.

Related

Metadata

Metadata

Assignees

Labels

P1High-priority user-facing bug, regression, or broken workflow.clawsweeper:fix-shape-clearClawSweeper found a clear likely implementation shape for this issue.clawsweeper:queueable-fixClawSweeper marked this issue as an existing queue_fix_pr work candidate.clawsweeper:source-reproClawSweeper found a high-confidence source-level issue reproduction.impact:message-lossChannel message delivery can be lost, duplicated, or misrouted.issue-rating: 🦞 diamond lobsterVery strong issue quality with high-confidence source-level or clear reproduction.

Type

No type

Fields

Priority

None yet

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions