Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.openclaw.ai/llms.txt

Use this file to discover all available pages before exploring further.

Cron is the Gateway’s built-in scheduler. It persists jobs, wakes the agent at the right time, and can deliver output back to a chat channel or webhook endpoint.

Quick start

1

Add a one-shot reminder

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
2

Check your jobs

openclaw cron list
openclaw cron show <job-id>
3

See run history

openclaw cron runs --id <job-id>

How cron works

  • Cron runs inside the Gateway process (not inside the model).
  • Job definitions persist at ~/.openclaw/cron/jobs.json so restarts do not lose schedules.
  • Runtime execution state persists next to it in ~/.openclaw/cron/jobs-state.json. If you track cron definitions in git, track jobs.json and gitignore jobs-state.json.
  • After the split, older OpenClaw versions can read jobs.json but may treat jobs as fresh because runtime fields now live in jobs-state.json.
  • When jobs.json is edited while the Gateway is running or stopped, OpenClaw compares the changed schedule fields with pending runtime slot metadata and clears stale nextRunAtMs values. Pure formatting or key-order-only rewrites preserve the pending slot.
  • All cron executions create background task records.
  • One-shot jobs (--at) auto-delete after success by default.
  • Isolated cron runs best-effort close tracked browser tabs/processes for their cron:<jobId> session when the run completes, so detached browser automation does not leave orphaned processes behind.
  • Isolated cron runs also guard against stale acknowledgement replies. If the first result is just an interim status update (on it, pulling everything together, and similar hints) and no descendant subagent run is still responsible for the final answer, OpenClaw re-prompts once for the actual result before delivery.
  • Isolated cron runs prefer structured execution-denial metadata from the embedded run, then fall back to known final summary/output markers such as SYSTEM_RUN_DENIED and INVALID_REQUEST, so a blocked command is not reported as a green run.
  • Isolated cron runs also treat run-level agent failures as job errors even when no reply payload is produced, so model/provider failures increment error counters and trigger failure notifications instead of clearing the job as successful.
Task reconciliation for cron is runtime-owned first, durable-history-backed second: an active cron task stays live while the cron runtime still tracks that job as running, even if an old child session row still exists. Once the runtime stops owning the job and the 5-minute grace window expires, maintenance checks persisted run logs and job state for the matching cron:<jobId>:<startedAt> run. If that durable history shows a terminal result, the task ledger is finalized from it; otherwise Gateway-owned maintenance can mark the task lost. Offline CLI audit can recover from durable history, but it does not treat its own empty in-process active-job set as proof that a Gateway-owned cron run is gone.

Schedule types

KindCLI flagDescription
at--atOne-shot timestamp (ISO 8601 or relative like 20m)
every--everyFixed interval
cron--cron5-field or 6-field cron expression with optional --tz
Timestamps without a timezone are treated as UTC. Add --tz America/New_York for local wall-clock scheduling. Recurring top-of-hour expressions 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.

Day-of-month and day-of-week use OR logic

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 ~5–6 times per month instead of 0–1 times per month. OpenClaw uses Croner’s default OR behavior here. 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.

Execution styles

Style--session valueRuns inBest for
Main sessionmainNext heartbeat turnReminders, system events
IsolatedisolatedDedicated cron:<jobId>Reports, background chores
Current sessioncurrentBound at creation timeContext-aware recurring work
Custom sessionsession:custom-idPersistent named sessionWorkflows that build on history
Main session jobs enqueue a system event and optionally wake the heartbeat (--wake now or --wake next-heartbeat). Those system events do not extend daily/idle reset freshness for the target session. Isolated jobs run a dedicated agent turn with a fresh session. Custom sessions (session:xxx) persist context across runs, enabling workflows like daily standups that build on previous summaries.
For isolated jobs, “fresh session” means a new transcript/session id for each run. OpenClaw may carry safe preferences such as thinking/fast/verbose settings, labels, and explicit user-selected model/auth overrides, but it does not inherit ambient conversation context from an older cron row: channel/group routing, send or queue policy, elevation, origin, or ACP runtime binding. Use current or session:<id> when a recurring job should deliberately build on the same conversation context.
For isolated jobs, runtime teardown now includes best-effort browser cleanup for that cron session. Cleanup failures are ignored so the actual cron result still wins.Isolated cron runs also dispose any bundled MCP runtime instances created for the job through the shared runtime-cleanup path. This matches how main-session and custom-session MCP clients are torn down, so isolated cron jobs do not leak stdio child processes or long-lived MCP connections across runs.
When isolated cron runs orchestrate subagents, delivery also prefers the final descendant output over stale parent interim text. If descendants are still running, OpenClaw suppresses that partial parent update instead of announcing it.For text-only Discord announce targets, OpenClaw sends the canonical final assistant text once instead of replaying both streamed/intermediate text payloads and the final answer. Media and structured Discord payloads are still delivered as separate payloads so attachments and components are not dropped.

Payload options for isolated jobs

--message
string
required
Prompt text (required for isolated).
--model
string
Model override; uses the selected allowed model for the job.
--thinking
string
Thinking level override.
--light-context
boolean
Skip workspace bootstrap file injection.
--tools
string
Restrict which tools the job can use, for example --tools exec,read.
--model uses the selected allowed model as that job’s primary model. It is not the same as a chat-session /model override: configured fallback chains still apply when the job primary fails. If the requested model is not allowed or cannot be resolved, cron fails the run with an explicit validation error instead of silently falling back to the job’s agent/default model selection. Cron jobs can also carry payload-level fallbacks. When present, that list replaces the configured fallback chain for the job. Use fallbacks: [] in the job payload/API when you want a strict cron run that tries only the selected model. If a job has --model but neither payload nor configured fallbacks, OpenClaw passes an explicit empty fallback override so the agent primary is not appended as a hidden extra retry target. Model-selection precedence for isolated jobs is:
  1. Gmail hook model override (when the run came from Gmail and that override is allowed)
  2. Per-job payload model
  3. User-selected stored cron session model override
  4. Agent/default model selection
Fast mode follows the resolved live selection too. If the selected model config has params.fastMode, isolated cron uses that by default. A stored session fastMode override still wins over config in either direction. If an isolated run hits a live model-switch handoff, cron retries with the switched provider/model and persists that live selection for the active run before retrying. When the switch also carries a new auth profile, cron persists that auth profile override for the active run too. Retries are bounded: after the initial attempt plus 2 switch retries, cron aborts instead of looping forever. Before an isolated cron run enters the agent runner, OpenClaw checks reachable local provider endpoints for configured api: "ollama" and api: "openai-completions" providers whose baseUrl is loopback, private-network, or .local. If that endpoint is down, the run is recorded as skipped with a clear provider/model error instead of starting a model call. The endpoint result is cached for 5 minutes, so many due jobs using the same dead local Ollama, vLLM, SGLang, or LM Studio server share one small probe instead of creating a request storm. Skipped provider-preflight runs do not increment execution-error backoff; enable failureAlert.includeSkipped when you want repeated skip notifications.

Delivery and output

ModeWhat happens
announceFallback-deliver final text to the target if the agent did not send
webhookPOST finished event payload to a URL
noneNo runner fallback delivery
Use --announce --channel telegram --to "-1001234567890" for channel delivery. For Telegram forum topics, use -1001234567890:topic:123; direct RPC/config callers may also pass delivery.threadId as a string or number. Slack/Discord/Mattermost targets should 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. For isolated jobs, chat delivery is shared. If a chat route is available, the agent can use the message tool even when the job uses --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. Failure notifications follow a separate destination path:
  • cron.failureDestination sets a global default for failure notifications.
  • job.delivery.failureDestination overrides that per job.
  • If neither is set and the job already delivers via announce, failure notifications now 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.

CLI examples

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

Webhooks

Gateway can expose HTTP webhook endpoints for external triggers. Enable in config:
{
  hooks: {
    enabled: true,
    token: "shared-secret",
    path: "/hooks",
  },
}

Authentication

Every request must include the hook token via header:
  • Authorization: Bearer <token> (recommended)
  • x-openclaw-token: <token>
Query-string tokens are rejected.
Enqueue a system event for the main session:
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"}'
text
string
required
Event description.
mode
string
default:"now"
now or next-heartbeat.
Run an isolated agent turn:
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.4"}'
Fields: message (required), name, agentId, wakeMode, deliver, channel, to, model, fallbacks, thinking, timeoutSeconds.
Custom hook names are resolved via hooks.mappings in config. Mappings can transform arbitrary payloads into wake or agent actions with templates or code transforms.
Keep hook endpoints behind loopback, tailnet, or trusted reverse proxy.
  • Use a dedicated hook token; do not reuse gateway auth tokens.
  • Keep hooks.path on a dedicated subpath; / is rejected.
  • Set hooks.allowedAgentIds to limit explicit agentId routing.
  • Keep hooks.allowRequestSessionKey=false unless you require caller-selected sessions.
  • If you enable hooks.allowRequestSessionKey, also set hooks.allowedSessionKeyPrefixes to constrain allowed session key shapes.
  • Hook payloads are wrapped with safety boundaries by default.

Gmail PubSub integration

Wire Gmail inbox triggers to OpenClaw via Google PubSub.
Prerequisites: gcloud CLI, gog (gogcli), OpenClaw hooks enabled, Tailscale for the public HTTPS endpoint.
openclaw webhooks gmail setup --account openclaw@gmail.com
This writes hooks.gmail config, enables the Gmail preset, and uses Tailscale Funnel for the push endpoint.

Gateway auto-start

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.

Manual one-time setup

1

Select the GCP project

Select the GCP project that owns the OAuth client used by gog:
gcloud auth login
gcloud config set project <project-id>
gcloud services enable gmail.googleapis.com pubsub.googleapis.com
2

Create topic and grant Gmail push access

gcloud pubsub topics create gog-gmail-watch
gcloud pubsub topics add-iam-policy-binding gog-gmail-watch \
  --member=serviceAccount:gmail-api-push@system.gserviceaccount.com \
  --role=roles/pubsub.publisher
3

Start the watch

gog gmail watch start \
  --account openclaw@gmail.com \
  --label INBOX \
  --topic projects/<project-id>/topics/gog-gmail-watch

Gmail model override

{
  hooks: {
    gmail: {
      model: "openrouter/meta-llama/llama-3.3-70b-instruct:free",
      thinking: "off",
    },
  },
}

Managing jobs

# List all jobs
openclaw cron list

# Show one job, including resolved delivery route
openclaw cron show <jobId>

# Edit a job
openclaw cron edit <jobId> --message "Updated prompt" --model "opus"

# Force run a job now
openclaw cron run <jobId>

# Run only if due
openclaw cron run <jobId> --due

# View run history
openclaw cron runs --id <jobId> --limit 50

# Delete a job
openclaw cron remove <jobId>

# Agent selection (multi-agent setups)
openclaw cron add --name "Ops sweep" --cron "0 6 * * *" --session isolated --message "Check ops queue" --agent ops
openclaw cron edit <jobId> --clear-agent
Model override note:
  • openclaw cron add|edit --model ... changes the job’s selected model.
  • If the model is allowed, that exact provider/model reaches the isolated agent run.
  • If it is not allowed or cannot be resolved, cron fails the run with an explicit validation error.
  • Configured fallback chains still apply because cron --model is a job primary, not a session /model override.
  • Payload fallbacks replaces configured fallbacks for that job; fallbacks: [] disables fallback and makes the run strict.
  • A plain --model with no explicit or configured fallback list does not fall through to the agent primary as a silent extra retry target.

Configuration

{
  cron: {
    enabled: true,
    store: "~/.openclaw/cron/jobs.json",
    maxConcurrentRuns: 1,
    retry: {
      maxAttempts: 3,
      backoffMs: [60000, 120000, 300000],
      retryOn: ["rate_limit", "overloaded", "network", "server_error"],
    },
    webhookToken: "replace-with-dedicated-webhook-token",
    sessionRetention: "24h",
    runLog: { maxBytes: "2mb", keepLines: 2000 },
  },
}
maxConcurrentRuns limits both scheduled cron dispatch and isolated agent-turn execution. 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. The runtime state sidecar is derived from cron.store: a .json store such as ~/clawd/cron/jobs.json uses ~/clawd/cron/jobs-state.json, while a store path without a .json suffix appends -state.json. If you hand-edit jobs.json, leave jobs-state.json out of source control. OpenClaw uses that sidecar for pending slots, active markers, last-run metadata, and the schedule identity that tells the scheduler when an externally edited job needs a fresh nextRunAtMs. Disable cron: cron.enabled: false or OPENCLAW_SKIP_CRON=1.
One-shot retry: transient errors (rate limit, overload, network, server error) retry up to 3 times with exponential backoff. Permanent errors disable immediately.Recurring retry: exponential backoff (30s to 60m) between retries. Backoff resets after the next successful run.
cron.sessionRetention (default 24h) prunes isolated run-session entries. cron.runLog.maxBytes / cron.runLog.keepLines auto-prune run-log files.

Troubleshooting

Command ladder

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
  • Check cron.enabled and OPENCLAW_SKIP_CRON env var.
  • Confirm the Gateway is running continuously.
  • For cron schedules, verify timezone (--tz) vs the host timezone.
  • reason: not-due in run output means manual run was checked with openclaw cron run <jobId> --due and the job was not due yet.
  • Delivery mode none means no runner fallback send is expected. The agent can still send directly with the message tool when a chat route is available.
  • Delivery target missing/invalid (channel/to) means outbound was skipped.
  • For Matrix, copied or legacy jobs with lowercased delivery.to room IDs can fail because Matrix room IDs are case-sensitive. Edit the job to the exact !room:server or room:!room:server value from Matrix.
  • Channel auth errors (unauthorized, Forbidden) mean delivery was blocked by credentials.
  • If the isolated run returns only the silent token (NO_REPLY / no_reply), OpenClaw suppresses direct outbound delivery and also suppresses the fallback queued summary path, so nothing is posted back to chat.
  • If the agent should message the user itself, check that the job has a usable route (channel: "last" with a previous chat, or an explicit channel/target).
  • Daily and idle reset freshness is not based on updatedAt; see Session management.
  • Cron wakeups, heartbeat runs, exec notifications, and gateway bookkeeping may update the session row for routing/status, but they do not extend sessionStartedAt or lastInteractionAt.
  • For legacy rows created before those fields existed, OpenClaw can recover sessionStartedAt from the transcript JSONL session header when the file is still available. Legacy idle rows without lastInteractionAt use that recovered start time as their idle baseline.
  • Cron without --tz uses the gateway host timezone.
  • at schedules without timezone are treated as UTC.
  • Heartbeat activeHours uses configured timezone resolution.