Cron vs Heartbeat? See Cron vs Heartbeat for guidance on when to use each.
Cron is the Gateway’s built-in scheduler. It persists jobs, wakes the agent at the right time, and can optionally deliver output back to a chat.
If you want “run this every morning” or “poke the agent in 20 minutes”, cron is the mechanism.
Troubleshooting: /automation/troubleshooting
~/.coderclaw/cron/ so restarts don’t lose schedules.cron:<jobId>, with delivery (announce by default or none).delivery.mode = "webhook" + delivery.to = "<url>".notify: true when cron.webhook is set, migrate those jobs to webhook delivery mode.Create a one-shot reminder, verify it exists, and run it immediately:
coderclaw cron add \
--name "Reminder" \
--at "2026-02-01T16:00:00Z" \
--session main \
--system-event "Reminder: check the cron docs draft" \
--wake now \
--delete-after-run
coderclaw cron list
coderclaw cron run <job-id>
coderclaw cron runs --id <job-id>
Schedule a recurring isolated job with delivery:
coderclaw cron add \
--name "Morning brief" \
--cron "0 7 * * *" \
--tz "America/Los_Angeles" \
--session isolated \
--message "Summarize overnight updates." \
--announce \
--channel slack \
--to "channel:C1234567890"
For the canonical JSON shapes and examples, see JSON schema for tool calls.
Cron jobs are persisted on the Gateway host at ~/.coderclaw/cron/jobs.json by default.
The Gateway loads the file into memory and writes it back on changes, so manual edits
are only safe when the Gateway is stopped. Prefer coderclaw cron add/edit or the cron
tool call API for changes.
Think of a cron job as: when to run + what to do.
schedule.kind = "at" (CLI: --at)schedule.kind = "every" or schedule.kind = "cron"sessionTarget: "main" → run during the next heartbeat with main context.sessionTarget: "isolated" → run a dedicated agent turn in cron:<jobId>.payload.kind = "systemEvent"payload.kind = "agentTurn"Optional: one-shot jobs (schedule.kind = "at") delete after success by default. Set
deleteAfterRun: false to keep them (they will disable after success).
A cron job is a stored record with:
announce, webhook, or none).agentId): run the job under a specific agent; if
missing or unknown, the gateway falls back to the default agent.Jobs are identified by a stable jobId (used by CLI/Gateway APIs).
In agent tool calls, jobId is canonical; legacy id is accepted for compatibility.
One-shot jobs auto-delete after success by default; set deleteAfterRun: false to keep them.
Cron supports three schedule kinds:
at: one-shot timestamp via schedule.at (ISO 8601).every: fixed interval (ms).cron: 5-field cron expression (or 6-field with seconds) with optional IANA timezone.Cron expressions use croner. If a timezone is omitted, the Gateway host’s
local timezone is used.
To reduce top-of-hour load spikes across many gateways, CoderClaw applies a
deterministic per-job stagger window of up to 5 minutes for recurring
top-of-hour expressions (for example 0 * * * *, 0 */2 * * *). Fixed-hour
expressions such as 0 7 * * * remain exact.
For any cron schedule, you can set an explicit stagger window with schedule.staggerMs
(0 keeps exact timing). CLI shortcuts:
--stagger 30s (or 1m, 5m) to set an explicit stagger window.--exact to force staggerMs = 0.Main jobs enqueue a system event and optionally wake the heartbeat runner.
They must use payload.kind = "systemEvent".
wakeMode: "now" (default): event triggers an immediate heartbeat run.wakeMode: "next-heartbeat": event waits for the next scheduled heartbeat.This is the best fit when you want the normal heartbeat prompt + main-session context. See Heartbeat.
Isolated jobs run a dedicated agent turn in session cron:<jobId>.
Key behaviors:
[cron:<jobId> <job name>] for traceability.delivery is omitted, isolated jobs announce a summary (delivery.mode = "announce").delivery.mode chooses what happens:
announce: deliver a summary to the target channel and post a brief summary to the main session.webhook: POST the finished event payload to delivery.to when the finished event includes a summary.none: internal only (no delivery, no main-session summary).wakeMode controls when the main-session summary posts:
now: immediate heartbeat.next-heartbeat: waits for the next scheduled heartbeat.Use isolated jobs for noisy, frequent, or “background chores” that shouldn’t spam your main chat history.
Two payload kinds are supported:
systemEvent: main-session only, routed through the heartbeat prompt.agentTurn: isolated-session only, runs a dedicated agent turn.Common agentTurn fields:
message: required text prompt.model / thinking: optional overrides (see below).timeoutSeconds: optional timeout override.Delivery config:
delivery.mode: none |
announce |
webhook. |
delivery.channel: last or a specific channel.delivery.to: channel-specific target (announce) or webhook URL (webhook mode).delivery.bestEffort: avoid failing the job if announce delivery fails.Announce delivery suppresses messaging tool sends for the run; use delivery.channel/delivery.to
to target the chat instead. When delivery.mode = "none", no summary is posted to the main session.
If delivery is omitted for isolated jobs, CoderClaw defaults to announce.
When delivery.mode = "announce", cron delivers directly via the outbound channel adapters.
The main agent is not spun up to craft or forward the message.
Behavior details:
HEARTBEAT_OK with no real content) are not delivered.delivery.bestEffort = true.delivery.mode = "announce".wakeMode: now triggers an immediate heartbeat and
next-heartbeat waits for the next scheduled heartbeat.When delivery.mode = "webhook", cron posts the finished event payload to delivery.to when the finished event includes a summary.
Behavior details:
cron.webhookToken is set, auth header is Authorization: Bearer <cron.webhookToken>.notify: true still post to cron.webhook (if configured), with a warning so you can migrate to delivery.mode = "webhook".Isolated jobs (agentTurn) can override the model and thinking level:
model: Provider/model string (e.g., anthropic/claude-sonnet-4-20250514) or alias (e.g., opus)thinking: Thinking level (off, minimal, low, medium, high, xhigh; GPT-5.2 + Codex models only)Note: You can set model on main-session jobs too, but it changes the shared main
session model. We recommend model overrides only for isolated jobs to avoid
unexpected context shifts.
Resolution priority:
hooks.gmail.model)Isolated jobs can deliver output to a channel via the top-level delivery config:
delivery.mode: announce (channel delivery), webhook (HTTP POST), or none.delivery.channel: whatsapp / telegram / discord / slack / mattermost (plugin) / signal / imessage / last.delivery.to: channel-specific recipient target.announce delivery is only valid for isolated jobs (sessionTarget: "isolated").
webhook delivery is valid for both main and isolated jobs.
If delivery.channel or delivery.to is omitted, cron can fall back to the main session’s
“last route” (the last place the agent replied).
Target format reminders:
channel:<id>, user:<id>) to avoid ambiguity.:topic: form (see below).Telegram supports forum topics via message_thread_id. For cron delivery, you can encode
the topic/thread into the to field:
-1001234567890 (chat id only)-1001234567890:topic:123 (preferred: explicit topic marker)-1001234567890:123 (shorthand: numeric suffix)Prefixed targets like telegram:... / telegram:group:... are also accepted:
telegram:group:-1001234567890:topic:123Use these shapes when calling Gateway cron.* tools directly (agent tool calls or RPC).
CLI flags accept human durations like 20m, but tool calls should use an ISO 8601 string
for schedule.at and milliseconds for schedule.everyMs.
One-shot, main session job (system event):
{
"name": "Reminder",
"schedule": { "kind": "at", "at": "2026-02-01T16:00:00Z" },
"sessionTarget": "main",
"wakeMode": "now",
"payload": { "kind": "systemEvent", "text": "Reminder text" },
"deleteAfterRun": true
}
Recurring, isolated job with delivery:
{
"name": "Morning brief",
"schedule": { "kind": "cron", "expr": "0 7 * * *", "tz": "America/Los_Angeles" },
"sessionTarget": "isolated",
"wakeMode": "next-heartbeat",
"payload": {
"kind": "agentTurn",
"message": "Summarize overnight updates."
},
"delivery": {
"mode": "announce",
"channel": "slack",
"to": "channel:C1234567890",
"bestEffort": true
}
}
Notes:
schedule.kind: at (at), every (everyMs), or cron (expr, optional tz).schedule.at accepts ISO 8601 (timezone optional; treated as UTC when omitted).everyMs is milliseconds.sessionTarget must be "main" or "isolated" and must match payload.kind.agentId, description, enabled, deleteAfterRun (defaults to true for at),
delivery.wakeMode defaults to "now" when omitted.{
"jobId": "job-123",
"patch": {
"enabled": false,
"schedule": { "kind": "every", "everyMs": 3600000 }
}
}
Notes:
jobId is canonical; id is accepted for compatibility.agentId: null in the patch to clear an agent binding.{ "jobId": "job-123", "mode": "force" }
{ "jobId": "job-123" }
~/.coderclaw/cron/jobs.json (Gateway-managed JSON).~/.coderclaw/cron/runs/<jobId>.jsonl (JSONL, auto-pruned).cron.store in config.{
cron: {
enabled: true, // default true
store: "~/.coderclaw/cron/jobs.json",
maxConcurrentRuns: 1, // default 1
webhook: "https://example.invalid/legacy", // deprecated fallback for stored notify:true jobs
webhookToken: "replace-with-dedicated-webhook-token", // optional bearer token for webhook mode
},
}
Webhook behavior:
delivery.mode: "webhook" with delivery.to: "https://..." per job.http:// or https:// URLs.cron.webhookToken is set, auth header is Authorization: Bearer <cron.webhookToken>.cron.webhookToken is not set, no Authorization header is sent.notify: true still use cron.webhook when present.Disable cron entirely:
cron.enabled: false (config)CODERCLAW_SKIP_CRON=1 (env)One-shot reminder (UTC ISO, auto-delete after success):
coderclaw cron add \
--name "Send reminder" \
--at "2026-01-12T18:00:00Z" \
--session main \
--system-event "Reminder: submit expense report." \
--wake now \
--delete-after-run
One-shot reminder (main session, wake immediately):
coderclaw cron add \
--name "Calendar check" \
--at "20m" \
--session main \
--system-event "Next heartbeat: check calendar." \
--wake now
Recurring isolated job (announce to WhatsApp):
coderclaw cron add \
--name "Morning status" \
--cron "0 7 * * *" \
--tz "America/Los_Angeles" \
--session isolated \
--message "Summarize inbox + calendar for today." \
--announce \
--channel whatsapp \
--to "+15551234567"
Recurring cron job with explicit 30-second stagger:
coderclaw cron add \
--name "Minute watcher" \
--cron "0 * * * * *" \
--tz "UTC" \
--stagger 30s \
--session isolated \
--message "Run minute watcher checks." \
--announce
Recurring isolated job (deliver to a Telegram topic):
coderclaw cron add \
--name "Nightly summary (topic)" \
--cron "0 22 * * *" \
--tz "America/Los_Angeles" \
--session isolated \
--message "Summarize today; send to the nightly topic." \
--announce \
--channel telegram \
--to "-1001234567890:topic:123"
Isolated job with model and thinking override:
coderclaw cron add \
--name "Deep analysis" \
--cron "0 6 * * 1" \
--tz "America/Los_Angeles" \
--session isolated \
--message "Weekly deep analysis of project progress." \
--model "opus" \
--thinking high \
--announce \
--channel whatsapp \
--to "+15551234567"
Agent selection (multi-agent setups):
# Pin a job to agent "ops" (falls back to default if that agent is missing)
coderclaw cron add --name "Ops sweep" --cron "0 6 * * *" --session isolated --message "Check ops queue" --agent ops
# Switch or clear the agent on an existing job
coderclaw cron edit <jobId> --agent ops
coderclaw cron edit <jobId> --clear-agent
Manual run (force is the default, use --due to only run when due):
coderclaw cron run <jobId>
coderclaw cron run <jobId> --due
Edit an existing job (patch fields):
coderclaw cron edit <jobId> \
--message "Updated prompt" \
--model "opus" \
--thinking low
Force an existing cron job to run exactly on schedule (no stagger):
coderclaw cron edit <jobId> --exact
Run history:
coderclaw cron runs --id <jobId> --limit 50
Immediate system event without creating a job:
coderclaw system event --mode now --text "Next heartbeat: check battery."
cron.list, cron.status, cron.add, cron.update, cron.removecron.run (force or due), cron.runs
For immediate system events without a job, use coderclaw system event.cron.enabled and CODERCLAW_SKIP_CRON.cron schedules: confirm timezone (--tz) vs the host timezone.at) jobs disable after a terminal run (ok, error, or skipped) and do not retry.-100…:topic:<id> so it’s explicit and unambiguous.telegram:... prefixes in logs or stored “last route” targets, that’s normal;
cron delivery accepts them and still parses topic IDs correctly.false (e.g. requester session is busy), the gateway retries up to 3 times with tracking via announceRetryCount.endedAt are force-expired to prevent stale entries from looping indefinitely.announceRetryCount values.