.claude/skills/openclaw-cron-job-creation/SKILL.md
Create and debug OpenClaw cron jobs by editing jobs.json directly. Use when: (1) gateway/cron subsystem fails with "TypeError: Cannot read properties of undefined (reading 'trim')" after adding jobs, (2) creating recurring cron schedule jobs (not one-shot "at" jobs), (3) openclaw cron CLI commands fail with missing env vars, (4) need to manually run/test a cron job. Covers the correct jobs.json schema, schedule field naming (expr NOT cron), env var requirements, and gateway restart.
npx skillsauth add Dbochman/dotfiles openclaw-cron-job-creationInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Creating OpenClaw cron jobs by editing ~/.openclaw/cron/jobs.json directly can cause
the gateway's cron subsystem to crash on startup with a misleading error if the JSON
schema doesn't match what the gateway expects.
[gateway/cron] failed to start: TypeError: Cannot read properties of undefined (reading 'trim')jobs.json and restarting gatewayopenclaw cron add CLI fails because it needs env vars (OPENCLAW_GATEWAY_TOKEN, OPENAI_API_KEY, ELEVENLABS_API_KEY, etc.) that are only available inside the gateway wrapper{
"id": "<uuid4>",
"agentId": "main",
"name": "My Cron Job",
"enabled": true,
"createdAtMs": 1771191000000,
"updatedAtMs": 1771191000000,
"schedule": {
"kind": "cron",
"expr": "0 7 * * *",
"tz": "America/New_York"
},
"sessionTarget": "isolated",
"wakeMode": "next-heartbeat",
"payload": {
"kind": "agentTurn",
"message": "Your agent prompt here"
},
"delivery": {
"mode": "announce",
"channel": "bluebubbles",
"to": "+15551234567"
},
"state": {}
}
| Field | Correct | Wrong | Notes |
|-------|---------|-------|-------|
| Cron expression | schedule.expr | schedule.cron | Source code: coerceSchedule() checks schedule.expr |
| Schedule type | schedule.kind: "cron" | - | Other kinds: "at", "every" |
| Timezone | schedule.tz | schedule.timezone | IANA format, optional |
// Recurring cron
{ "kind": "cron", "expr": "0 7 * * *", "tz": "America/New_York" }
// One-shot at time
{ "kind": "at", "at": "2026-04-01T12:00:00.000Z" }
// Interval
{ "kind": "every", "everyMs": 3600000 }
CRITICAL: The gateway holds jobs.json in memory and writes its in-memory copy back to disk whenever it updates job state (lastStatus, lastRunAtMs, etc.). If you edit the file while the gateway is running, the gateway will overwrite your changes on the next state write.
launchctl bootout gui/$(id -u)/ai.openclaw.gatewaypgrep -fl openclaw-gateway (should return nothing)cp ~/.openclaw/cron/jobs.json ~/.openclaw/cron/jobs.json.bakjobs array (use Python script for complex prompts)launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/ai.openclaw.gateway.plist/tmp/openclaw/openclaw-YYYY-MM-DD.log for cron startup line:
{'enabled': True, 'jobs': N, 'nextWakeAtMs': ...}failed to start: TypeError: Cannot read properties of undefined (reading 'trim')Do NOT use launchctl kickstart -k for config changes — the restart is too fast and the
gateway may read the old file from OS disk cache before the write flushes. Stop, edit, start.
Use openclaw cron run with secrets sourced and a long timeout:
set -a && source ~/.openclaw/.secrets-cache && set +a && \
PATH=/opt/homebrew/bin:/opt/homebrew/opt/node@22/bin:$PATH \
openclaw cron run <jobId> --timeout 300000 --expect-final
Key flags:
--timeout 300000 — 5 minutes (default 30s is too short for most jobs)--expect-final — waits for the job to complete and returns {"ok":true,"ran":true}This is the correct way to manually trigger a job. It:
jobs.jsonDo NOT use openclaw agent --deliver for manual cron testing — it spawns independent
async agents with no dedup. Each invocation creates a separate session that runs all side
effects (labeling, archiving, sending) independently, causing duplicate deliveries if
retried.
When removing a job from jobs.json, also delete its run state file:
trash ~/.openclaw/cron/runs/<jobId>.jsonl
The cron subsystem persists nextRunAtMs in these JSONL files. If a run file exists with
a future nextRunAtMs, the job will keep executing even after its definition is removed
from jobs.json. See the openclaw-cron-ghost-jobs skill for diagnosis and cleanup.
deleteAfterRun: Only for one-shot "at" jobs (auto-added by gateway for "at" kind)timeoutSeconds: Goes inside payload, not at the top level (gateway handles via coercePayload)After restarting the gateway, check the detailed log:
grep "cron" /tmp/openclaw/openclaw-$(date +%Y-%m-%d).log | python3 -c "
import sys, json
for line in sys.stdin:
d = json.loads(line)
print(d.get('time','') + ': ' + str(d.get('1', d.get('0','')))[:150])
"
Success shows: {'enabled': True, 'jobs': <count>, 'nextWakeAtMs': <timestamp>}
Adding a daily Gmail triage job for Julia:
import json, uuid, time
with open("/Users/dbochman/.openclaw/cron/jobs.json") as f:
data = json.load(f)
job = {
"id": str(uuid.uuid4()),
"agentId": "main",
"name": "Julia Gmail Morning Triage",
"enabled": True,
"createdAtMs": int(time.time() * 1000),
"updatedAtMs": int(time.time() * 1000),
"schedule": {
"kind": "cron",
"expr": "0 7 * * *", # NOT "cron": "0 7 * * *"
"tz": "America/New_York"
},
"sessionTarget": "isolated",
"wakeMode": "next-heartbeat",
"payload": {
"kind": "agentTurn",
"message": "Your prompt here..."
},
"delivery": {
"mode": "announce",
"channel": "bluebubbles",
"to": "+1XXXXXXXXXX"
},
"state": {}
}
data["jobs"].append(job)
with open("/Users/dbochman/.openclaw/cron/jobs.json", "w") as f:
json.dump(data, f, indent=2)
"Unsupported channel: X"
after a channel migration is a telltale sign the gateway wrote back stale config.bluebubbles (NOT imessage). All cron jobs use
"delivery": {"channel": "bluebubbles", ...}. The dotfiles source copy is at
~/repos/dotfiles/openclaw/cron/jobs.json — keep it in sync with Mini's
~/.openclaw/cron/jobs.json.~/Applications/OpenClawGateway.app/Contents/MacOS/OpenClawGateway) sources secrets from ~/.openclaw/.secrets-cache (KEY=VALUE format, chmod 600)j["name"] inside SSH-piped python one-liners cause NameError due to shell escaping — use script files (scp + run) for complex JSON manipulationdelivery.to field accepts both E.164 phone numbers and OpenClaw contact UUIDsdeleteAfterRun: true for "at" schedule jobs during normalizationloader-n6BPnYom.js lines 9183-9470 contain cron normalization logictools
Use exact configured Reolink cameras through the local Home Hub for availability and power status, fresh stills, visual commentary, protected Dylan/Julia/household sharing, and reversible spotlight control. Supports trusted owner tasks and explicitly scoped proactive automations; not for Nest or Ring cameras, arbitrary recipients, recordings, account changes, or raw camera APIs.
data-ai
Privately manage Dylan and Julia's household plant inventory and care history by physical location, bed, and exact Flower Cam view. Use for confirmed plant onboarding from camera conversations, camera- or bed-filtered inventory, record corrections, individual or whole-bed care, and private filtered exports. Pair with reolink-camera when an owner asks about plants visible in Flower Cam images.
testing
Inspect and control the physically secured Reachy Mini at Crosstown through ClawBody. Use for requests to check Reachy, look around, express an emotion, play any official emotion or dance preset, speak proactively, mute or unmute its microphone, stop movement, or describe what its camera sees.
tools
Handle Reachy/iMessage handoffs, selective durable memory, forgetting, and diagnostics; automatic context comes from the gateway plugin.