Skip to content

Cron Jobs

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.

  • Cron runs inside the Gateway (not inside the model).
  • Jobs persist under ~/.openclaw/cron/ so restarts don’t lose schedules.
  • Two execution styles:
    • Main session: enqueue a system event, then run on the next heartbeat.
    • Isolated: run a dedicated agent turn in cron:<jobId>, optionally deliver output.
  • Wakeups are first-class: a job can request “wake now” vs “next heartbeat”.

Create a one-shot reminder, verify it exists, and run it immediately:

Terminal window
openclaw 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
openclaw cron list
openclaw cron run <job-id> --force
openclaw cron runs --id <job-id>

Schedule a recurring isolated job with delivery:

Terminal window
openclaw cron add \
--name "Morning brief" \
--cron "0 7 * * *" \
--tz "America/Los_Angeles" \
--session isolated \
--message "Summarize overnight updates." \
--deliver \
--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 ~/.openclaw/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 openclaw cron add/edit or the cron tool call API for changes.

Think of a cron job as: when to run + what to do.

  1. Choose a schedule

    • One-shot reminder → schedule.kind = "at" (CLI: --at)
    • Repeating job → schedule.kind = "every" or schedule.kind = "cron"
    • If your ISO timestamp omits a timezone, it is treated as UTC.
  2. Choose where it runs

    • sessionTarget: "main" → run during the next heartbeat with main context.
    • sessionTarget: "isolated" → run a dedicated agent turn in cron:<jobId>.
  3. Choose the payload

    • Main session → payload.kind = "systemEvent"
    • Isolated session → payload.kind = "agentTurn"

Optional: deleteAfterRun: true removes successful one-shot jobs from the store.

A cron job is a stored record with:

  • a schedule (when it should run),
  • a payload (what it should do),
  • optional delivery (where output should be sent).
  • optional agent binding (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. Jobs can optionally auto-delete after a successful one-shot run via deleteAfterRun: true.

Cron supports three schedule kinds:

  • at: one-shot timestamp (ms since epoch). Gateway accepts ISO 8601 and coerces to UTC.
  • every: fixed interval (ms).
  • cron: 5-field cron expression with optional IANA timezone.

Cron expressions use croner. If a timezone is omitted, the Gateway host’s local timezone is used.

Main jobs enqueue a system event and optionally wake the heartbeat runner. They must use payload.kind = "systemEvent".

  • wakeMode: "next-heartbeat" (default): event waits for the next scheduled heartbeat.
  • wakeMode: "now": event triggers an immediate heartbeat run.

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:

  • Prompt is prefixed with [cron:<jobId> <job name>] for traceability.
  • Each run starts a fresh session id (no prior conversation carry-over).
  • A summary is posted to the main session (prefix Cron, configurable).
  • wakeMode: "now" triggers an immediate heartbeat after posting the summary.
  • If payload.deliver: true, output is delivered to a channel; otherwise it stays internal.

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.
  • deliver: true to send output to a channel target.
  • channel: last or a specific channel.
  • to: channel-specific target (phone/chat/channel id).
  • bestEffortDeliver: avoid failing the job if delivery fails.

Isolation options (only for session=isolated):

  • postToMainPrefix (CLI: --post-prefix): prefix for the system event in main.
  • postToMainMode: summary (default) or full.
  • postToMainMaxChars: max chars when postToMainMode=full (default 8000).

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:

  1. Job payload override (highest)
  2. Hook-specific defaults (e.g., hooks.gmail.model)
  3. Agent config default

Isolated jobs can deliver output to a channel. The job payload can specify:

  • channel: whatsapp / telegram / discord / slack / mattermost (plugin) / signal / imessage / last
  • to: channel-specific recipient target

If channel or to is omitted, cron can fall back to the main session’s “last route” (the last place the agent replied).

Delivery notes:

  • If to is set, cron auto-delivers the agent’s final output even if deliver is omitted.
  • Use deliver: true when you want last-route delivery without an explicit to.
  • Use deliver: false to keep output internal even if a to is present.

Target format reminders:

  • Slack/Discord/Mattermost (plugin) targets should use explicit prefixes (e.g. channel:<id>, user:<id>) to avoid ambiguity.
  • Telegram topics should use the :topic: form (see below).

Telegram delivery targets (topics / forum threads)

Section titled “Telegram delivery targets (topics / forum threads)”

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:123

Use these shapes when calling Gateway cron.* tools directly (agent tool calls or RPC). CLI flags accept human durations like 20m, but tool calls use epoch milliseconds for atMs and everyMs (ISO timestamps are accepted for at times).

One-shot, main session job (system event):

{
"name": "Reminder",
"schedule": { "kind": "at", "atMs": 1738262400000 },
"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.",
"deliver": true,
"channel": "slack",
"to": "channel:C1234567890",
"bestEffortDeliver": true
},
"isolation": { "postToMainPrefix": "Cron", "postToMainMode": "summary" }
}

Notes:

  • schedule.kind: at (atMs), every (everyMs), or cron (expr, optional tz).
  • atMs and everyMs are epoch milliseconds.
  • sessionTarget must be "main" or "isolated" and must match payload.kind.
  • Optional fields: agentId, description, enabled, deleteAfterRun, isolation.
  • wakeMode defaults to "next-heartbeat" when omitted.
{
"jobId": "job-123",
"patch": {
"enabled": false,
"schedule": { "kind": "every", "everyMs": 3600000 }
}
}

Notes:

  • jobId is canonical; id is accepted for compatibility.
  • Use agentId: null in the patch to clear an agent binding.
{ "jobId": "job-123", "mode": "force" }
{ "jobId": "job-123" }
  • Job store: ~/.openclaw/cron/jobs.json (Gateway-managed JSON).
  • Run history: ~/.openclaw/cron/runs/<jobId>.jsonl (JSONL, auto-pruned).
  • Override store path: cron.store in config.
{
cron: {
enabled: true, // default true
store: "~/.openclaw/cron/jobs.json",
maxConcurrentRuns: 1, // default 1
},
}

Disable cron entirely:

  • cron.enabled: false (config)
  • OPENCLAW_SKIP_CRON=1 (env)

One-shot reminder (UTC ISO, auto-delete after success):

Terminal window
openclaw 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):

Terminal window
openclaw cron add \
--name "Calendar check" \
--at "20m" \
--session main \
--system-event "Next heartbeat: check calendar." \
--wake now

Recurring isolated job (deliver to WhatsApp):

Terminal window
openclaw cron add \
--name "Morning status" \
--cron "0 7 * * *" \
--tz "America/Los_Angeles" \
--session isolated \
--message "Summarize inbox + calendar for today." \
--deliver \
--channel whatsapp \
--to "+15551234567"

Recurring isolated job (deliver to a Telegram topic):

Terminal window
openclaw cron add \
--name "Nightly summary (topic)" \
--cron "0 22 * * *" \
--tz "America/Los_Angeles" \
--session isolated \
--message "Summarize today; send to the nightly topic." \
--deliver \
--channel telegram \
--to "-1001234567890:topic:123"

Isolated job with model and thinking override:

Terminal window
openclaw 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 \
--deliver \
--channel whatsapp \
--to "+15551234567"

Agent selection (multi-agent setups):

Terminal window
# Pin a job to agent "ops" (falls back to default if that agent is missing)
openclaw 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
openclaw cron edit <jobId> --agent ops
openclaw cron edit <jobId> --clear-agent

Manual run (debug):

Terminal window
openclaw cron run <jobId> --force

Edit an existing job (patch fields):

Terminal window
openclaw cron edit <jobId> \
--message "Updated prompt" \
--model "opus" \
--thinking low

Run history:

Terminal window
openclaw cron runs --id <jobId> --limit 50

Immediate system event without creating a job:

Terminal window
openclaw system event --mode now --text "Next heartbeat: check battery."
  • cron.list, cron.status, cron.add, cron.update, cron.remove
  • cron.run (force or due), cron.runs For immediate system events without a job, use openclaw system event.
  • Check cron is enabled: cron.enabled and OPENCLAW_SKIP_CRON.
  • Check the Gateway is running continuously (cron runs inside the Gateway process).
  • For cron schedules: confirm timezone (--tz) vs the host timezone.
  • For forum topics, use -100…:topic:<id> so it’s explicit and unambiguous.
  • If you see telegram:... prefixes in logs or stored “last route” targets, that’s normal; cron delivery accepts them and still parses topic IDs correctly.