docs/automation/cron-jobs.md
Cron is the Gateway's built-in scheduler. It persists jobs, wakes the agent at the right time, and can deliver output to a chat channel, a webhook, or nowhere.
--at) auto-delete after success by default; pass --keep-after-run to keep them.--timeout-seconds when set. Otherwise, isolated/detached agent-turn jobs are bounded by cron's own 60-minute watchdog before the underlying agent-turn timeout (agents.defaults.timeoutSeconds, default 48 hours) would ever apply; command jobs default to 10 minutes.openclaw agent from system cron or another external scheduler, wrap it with a hard-kill escalation even though the CLI already handles SIGTERM/SIGINT. Gateway-backed runs ask the Gateway to abort accepted runs; local and embedded fallback runs get the same abort signal. For GNU timeout, prefer timeout -k 60 600 openclaw agent ... over plain timeout 600 ... — the -k value is the backstop if the process cannot drain in time. For systemd units, use a SIGTERM stop signal with a grace window (TimeoutStopSec) before the final kill. Reusing a --run-id while the original Gateway run is still active reports the duplicate as in-flight instead of starting a second run.| Kind | CLI flag | Description |
|---|---|---|
at | --at | One-shot timestamp (ISO 8601 or relative like 20m) |
every | --every | Fixed interval (10m, 1h, 1d) |
cron | --cron | 5-field or 6-field cron expression with optional --tz |
on-exit | --on-exit | Fire once when a watched command exits (event trigger; survives turn teardown; optional --on-exit-cwd) |
Timestamps without a timezone are treated as UTC. Add --tz America/New_York to interpret an offset-less --at datetime, or to evaluate a cron expression, in that IANA timezone. Cron expressions without --tz use the Gateway host timezone. --tz is not valid with --every or --on-exit.
Recurring top-of-hour expressions (minute 0 with a wildcard hour field) are automatically staggered by up to 5 minutes to reduce load spikes. Use --exact to force precise timing, or --stagger 30s for an explicit window (cron schedules only).
Cron expressions are parsed by croner. When both the day-of-month and day-of-week fields are non-wildcard, croner matches when either field matches, not both. This is standard Vixie cron behavior.
# Intended: "9 AM on the 15th, only if it's a Monday"
# Actual: "9 AM on every 15th, AND 9 AM on every Monday"
0 9 15 * 1
This fires roughly 5-6 times a month instead of 0-1 times a month. To require both conditions, use croner's + day-of-week modifier (0 9 15 * +1), or schedule on one field and guard the other in your job's prompt or command.
An event trigger adds a headless condition script to an every or cron schedule. Cron evaluates the script when the job is due and runs the normal payload only when the script returns fire: true:
{
schedule: { kind: "every", everyMs: 30000 },
trigger: {
// Fires only when the observed status differs from the last evaluation.
script: "const res = await tools.call('exec', { command: 'gh pr checks 123 --json state -q \\'.[].state\\' | sort -u' }); const status = String(res?.result?.details?.aggregated ?? '').trim(); json({ fire: status !== trigger.state?.status, message: `PR 123 CI: ${trigger.state?.status ?? 'unknown'} -> ${status}`, state: { status } });",
once: false,
},
payload: { kind: "agentTurn", message: "Investigate the CI status change." },
}
The script must return { fire, message?, state? }. The previous JSON state is available as the deeply frozen trigger.state; return a new state value to persist it. State is capped at 16 KB. When a firing result includes message, cron appends it to the system-event text or agent-turn message before execution. once: true disables the job after its first successful fired payload.
fire: false persists evaluation state and counters, then reschedules without creating run history. If a fired payload run fails, the returned state is not persisted — the next evaluation sees the previous state and can fire again, so write scripts as read-only checks and keep actions in the payload. Trigger schedules have a configurable minimum interval (30 seconds by default). Each evaluation has a 30-second wall-clock budget and up to 5 tool calls.
Create a watcher from a local script file (- reads the script from stdin):
openclaw cron add \
--name "PR CI watcher" \
--every 30s \
--trigger-script ./watch-pr-ci.js \
--message "Respond to the CI status change" \
--session isolated
Every job carries exactly one payload kind, chosen by flag:
| Payload | Flag | Runs |
|---|---|---|
| System event | --system-event <text> | Enqueued into the main session, no model call by itself |
| Agent message | --message <text> | A model-backed agent turn |
| Command | --command <shell> or --command-argv <json> | A shell/process on the Gateway host, no model call |
--model sets the job's primary model; it does not replace a session /model override, so configured fallback chains still apply on top of it. An unresolved or disallowed model fails the run with an explicit validation error rather than silently falling back to the default. If a job has --model but no explicit or configured fallback list, OpenClaw passes an empty fallback override instead of silently appending the agent primary as a hidden retry target.
Model-selection precedence for isolated jobs, highest first:
model (explicit config; a disallowed model fails the run)Fast mode follows the resolved live selection. If the selected model config has params.fastMode, isolated cron uses it by default; a stored session fastMode override (then an agent fastModeDefault) still wins over model config either direction. Auto mode uses the model's params.fastAutoOnSeconds cutoff, defaulting to 60 seconds.
If a run hits a live model-switch handoff, cron retries with the switched provider/model and persists that selection (and any new auth profile) for the active run. Retries are bounded: after the initial attempt plus 2 switch retries, cron aborts instead of looping.
Before an isolated run starts, OpenClaw checks reachable local endpoints for configured api: "ollama" and api: "openai-completions" providers whose baseUrl is loopback, private-network, or .local. This preflight walks the job's configured fallback chain and only marks the run skipped once every candidate is unreachable; --fallbacks "" keeps that walk strict to just the primary model. A down endpoint records the run as skipped with a clear error instead of starting a model call. The result is cached for 5 minutes per endpoint (not per job or model), so many due jobs sharing a dead local Ollama/vLLM/SGLang/LM Studio server cost one probe instead of a request storm. Skipped preflight runs do not increment execution-error backoff; set failureAlert.includeSkipped to opt into repeated skip alerts.
Command payloads run deterministic scripts inside the Gateway scheduler without starting a model-backed turn. They execute on the Gateway host, capture stdout/stderr, record the run in cron history, and reuse the same announce, webhook, and none delivery modes as agent-turn jobs.
openclaw cron create "*/15 * * * *" \
--name "Queue depth probe" \
--command "scripts/check-queue.sh" \
--command-cwd "/srv/app" \
--announce \
--channel telegram \
--to "-1001234567890"
--command <shell> stores argv: ["sh", "-lc", <shell>]. Use --command-argv '["node","scripts/report.mjs"]' for exact argv execution without shell parsing. Optional --command-env KEY=VALUE (repeatable), --command-input, --timeout-seconds (default 10 minutes), --no-output-timeout-seconds, and --output-max-bytes control the process environment, stdin, and output bounds.
Delivered text is derived from process output: non-empty stdout wins; if stdout is empty and stderr is non-empty, stderr is delivered; if both are present, cron sends a small stdout: / stderr: block. Exit code 0 records the run ok; non-zero exit, signal, timeout, or no-output timeout records error and can trigger failure alerts. A command that prints only NO_REPLY uses the normal cron silent-token suppression and posts nothing back to chat.
| Style | --session value | Runs in | Best for |
|---|---|---|---|
| Main session | main | Dedicated cron wake lane | Reminders, system events |
| Isolated | isolated | Dedicated cron:<jobId> | Reports, background chores |
| Current session | current | Bound at creation time | Context-aware recurring work |
| Custom session | session:custom-id | Persistent named session | Workflows that build on history |
Main-session cron events are self-contained system-event reminders. They do not automatically include the default heartbeat prompt's "Read HEARTBEAT.md" instruction; say that explicitly in the cron event text if a reminder should consult `HEARTBEAT.md`.
For text-only Discord announce targets, OpenClaw sends the canonical final assistant text once instead of replaying both streamed/intermediate text and the final answer. Media and structured Discord payloads are still delivered separately so attachments and components are not dropped.
| Mode | What happens |
|---|---|
announce | Fallback-deliver final text to the target if the agent did not send |
webhook | POST finished event payload to a URL |
none | No runner fallback delivery |
Use --announce --channel telegram --to "-1001234567890" for channel delivery. For Telegram forum topics, use -1001234567890:topic:123; OpenClaw also accepts the Telegram-owned -1001234567890:123 shorthand. Direct RPC/config callers may pass delivery.threadId as a string or number. Slack/Discord/Mattermost targets use explicit prefixes (channel:<id>, user:<id>). Matrix room IDs are case-sensitive; use the exact room ID or room:!room:server form from Matrix.
When announce delivery uses channel: "last" or omits channel, a provider-prefixed target such as telegram:123 can select the channel before cron falls back to session history or a single configured channel. Only prefixes advertised by the loaded plugin are provider selectors. If delivery.channel is explicit, the target prefix must name the same provider; channel: "whatsapp" with to: "telegram:123" is rejected instead of letting WhatsApp interpret the Telegram ID as a phone number. Target-kind and service prefixes (channel:<id>, user:<id>, imessage:<handle>, sms:<number>) stay channel-owned target syntax, not provider selectors.
For isolated jobs, chat delivery is shared: if a chat route is available, the agent can use the message tool even with --no-deliver. If the agent sends to the configured/current target, OpenClaw skips the fallback announce. Otherwise announce, webhook, and none only control what the runner does with the final reply after the agent turn.
When an agent creates an isolated reminder from an active chat, OpenClaw stores the preserved live delivery target for the fallback announce route. Internal session keys may be lowercase; provider delivery targets are not reconstructed from those keys when current chat context is available.
Implicit announce delivery uses configured channel allowlists to validate and reroute stale targets. DM pairing-store approvals are not fallback automation recipients; set delivery.to or configure the channel allowFrom entry when a scheduled job should proactively send to a DM.
Failure notifications follow a separate destination path:
cron.failureDestination sets a global default for failure notifications.job.delivery.failureDestination overrides that per job.announce, failure notifications fall back to that primary announce target.delivery.failureDestination is only supported on sessionTarget="isolated" jobs unless the primary delivery mode is webhook.failureAlert.includeSkipped: true opts a job or global cron alert policy into repeated skipped-run alerts. Skipped runs keep a separate consecutive-skip counter, so they do not affect execution-error backoff.openclaw cron edit exposes per-job alert tuning: --failure-alert/--no-failure-alert, --failure-alert-after <n>, --failure-alert-channel, --failure-alert-to, --failure-alert-cooldown, --failure-alert-include-skipped/--failure-alert-exclude-skipped, --failure-alert-mode, and --failure-alert-account-id.Cron jobs do not infer a reply language from channel, locale, or previous messages. Put the language rule in the scheduled message or template:
openclaw cron edit <jobId> \
--message "Summarize the updates. Respond in Chinese; keep URLs, code, and product names unchanged."
For template files, keep the language instruction in the rendered prompt and verify placeholders such as {{language}} are filled before the job runs. If the output mixes languages, make the rule explicit, for example: "Use Chinese for narrative text and keep technical terms in English."
# List all jobs
openclaw cron list
# Get one stored job as JSON
openclaw cron get <jobId>
# Show one job, including resolved delivery route
openclaw cron show <jobId>
# Enable/disable without deleting
openclaw cron enable <jobId>
openclaw cron disable <jobId>
# Edit a job
openclaw cron edit <jobId> --message "Updated prompt" --model "opus"
# Force run a job now
openclaw cron run <jobId>
# Force run a job now and wait for its terminal status
openclaw cron run <jobId> --wait --wait-timeout 10m --poll-interval 2s
# Run only if due
openclaw cron run <jobId> --due
# View run history
openclaw cron runs --id <jobId> --limit 50
# View one exact run
openclaw cron runs --id <jobId> --run-id <runId>
# Delete a job
openclaw cron remove <jobId>
# Agent selection (multi-agent setups)
openclaw cron create "0 6 * * *" "Check ops queue" --name "Ops sweep" --session isolated --agent ops
openclaw cron edit <jobId> --clear-agent
openclaw cron run <jobId> returns after enqueueing the manual run. Use --wait for shutdown hooks, maintenance scripts, or other automation that must block until the queued run finishes; it polls the returned runId (default timeout 10m, poll interval 2s) and exits 0 for status ok, non-zero for error, skipped, or a wait timeout.
The agent cron tool returns compact job summaries (id, name, enabled, nextRunAtMs, scheduleKind, lastRunStatus) from cron(action: "list"); use cron(action: "get", jobId: "...") for one full job definition. Direct Gateway callers can pass compact: true to cron.list; omitting it preserves the full response with delivery previews.
openclaw cron create is an alias for openclaw cron add. New jobs can use a positional schedule ("0 9 * * 1", "every 1h", "20m", or an ISO timestamp) followed by a positional agent prompt. Use --webhook <url> on cron add|create or cron edit to POST the finished run payload to an HTTP endpoint; webhook delivery cannot combine with chat delivery flags (--announce, --channel, --to, --thread-id, --account). On cron edit, --clear-channel, --clear-to, --clear-thread-id, and --clear-account unset those routing fields individually (each rejected alongside its matching set flag) — distinct from --no-deliver, which only disables runner fallback delivery.
openclaw cron add|edit --model ... changes the job's selected model.cron.update payload patches can set model: null to clear a stored job model override.openclaw cron edit <job-id> --clear-model clears that override from the CLI (same effect as the model: null patch) and cannot combine with --model.--model is a job primary, not a session /model override.openclaw cron add|edit --fallbacks ... sets payload fallbacks, replacing configured fallbacks for that job; --fallbacks "" disables fallback and makes the run strict. openclaw cron edit <job-id> --clear-fallbacks clears the per-job override.--model with no explicit or configured fallback list does not fall through to the agent primary as a silent extra retry target.Gateway can expose HTTP webhook endpoints for external triggers. Enable in config:
{
hooks: {
enabled: true,
token: "shared-secret",
path: "/hooks",
},
}
Every request must include the hook token via header:
Authorization: Bearer <token> (recommended)x-openclaw-token: <token>Query-string tokens are rejected.
<AccordionGroup> <Accordion title="POST /hooks/wake"> Enqueue a system event for the main session:```bash
curl -X POST http://127.0.0.1:18789/hooks/wake \
-H 'Authorization: Bearer SECRET' \
-H 'Content-Type: application/json' \
-d '{"text":"New email received","mode":"now"}'
```
<ParamField path="text" type="string" required>
Event description.
</ParamField>
<ParamField path="mode" type="string" default="now">
`now` or `next-heartbeat`.
</ParamField>
```bash
curl -X POST http://127.0.0.1:18789/hooks/agent \
-H 'Authorization: Bearer SECRET' \
-H 'Content-Type: application/json' \
-d '{"message":"Summarize inbox","name":"Email","model":"openai/gpt-5.5"}'
```
Fields: `message` (required), `name`, `agentId`, `sessionKey` (requires `hooks.allowRequestSessionKey=true`), `idempotencyKey`, `wakeMode`, `deliver`, `channel`, `to`, `model`, `thinking`, `timeoutSeconds`.
hooks.path on a dedicated subpath; / is rejected.hooks.allowedAgentIds to limit which effective agent a hook can target, including the default agent when agentId is omitted.hooks.allowRequestSessionKey=false unless you require caller-selected sessions.hooks.allowRequestSessionKey, also set hooks.allowedSessionKeyPrefixes to constrain allowed session key shapes.Wire Gmail inbox triggers to OpenClaw via Google PubSub.
<Note> **Prerequisites:** `gcloud` CLI, `gog` (gogcli), OpenClaw hooks enabled, Tailscale for the public HTTPS endpoint. </Note>openclaw webhooks gmail setup --account [email protected]
This writes hooks.gmail config, enables the Gmail preset, and defaults to Tailscale Funnel for the push endpoint (--tailscale funnel|serve|off).
When hooks.enabled=true and hooks.gmail.account is set, the Gateway starts gog gmail watch serve on boot and auto-renews the watch. Set OPENCLAW_SKIP_GMAIL_WATCHER=1 to opt out.
```bash
gcloud auth login
gcloud config set project <project-id>
gcloud services enable gmail.googleapis.com pubsub.googleapis.com
```
{
hooks: {
gmail: {
model: "openrouter/meta-llama/llama-3.3-70b-instruct:free",
thinking: "off",
},
},
}
{
cron: {
enabled: true,
store: "~/.openclaw/cron/jobs.json",
maxConcurrentRuns: 8,
triggers: {
enabled: false,
minIntervalMs: 30000,
},
retry: {
maxAttempts: 3,
backoffMs: [30000, 60000, 300000],
retryOn: ["rate_limit", "overloaded", "network", "timeout", "server_error"],
},
webhookToken: "replace-with-dedicated-webhook-token",
sessionRetention: "24h",
runLog: { maxBytes: "2mb", keepLines: 2000 },
},
}
The retry values above are the defaults: up to 3 retries with 30s/60s/5m backoff, retrying all five transient categories. webhookToken is sent as Authorization: Bearer <token> on cron webhook POSTs.
maxConcurrentRuns limits both scheduled cron dispatch and isolated agent-turn execution, and defaults to 8. Isolated cron agent turns use the queue's dedicated cron-nested execution lane internally, so raising this value lets independent cron LLM runs progress in parallel instead of only starting their outer cron wrappers. The shared non-cron nested lane is not widened by this setting.
cron.store is a logical store key and doctor migration path, not a live JSON file to hand-edit. Job data lives in SQLite; use the CLI or Gateway API for changes.
Disable cron: cron.enabled: false or OPENCLAW_SKIP_CRON=1.
**Recurring retry**: consecutive execution errors back off on an extended schedule (30s, 60s, 5m, 15m, 60m). Backoff resets after the next successful run.
openclaw status
openclaw gateway status
openclaw cron status
openclaw cron list
openclaw cron runs --id <jobId> --limit 20
openclaw system heartbeat last
openclaw logs --follow
openclaw doctor