Skip to main content

Scheduled Tasks (Cron)

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

# 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

# Check your jobs
openclaw cron list

# See run history
openclaw cron runs --id <job-id>

How cron works

  • Cron runs inside the Gateway process (not inside the model).
  • Jobs persist at ~/.openclaw/cron/jobs.json so restarts do not lose schedules.
  • All cron executions create background task records.
  • One-shot jobs (--at) auto-delete after success by default.

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.

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). 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.

Payload options for isolated jobs

  • --message: prompt text (required for isolated)
  • --model / --thinking: model and thinking level overrides
  • --light-context: skip workspace bootstrap file injection
  • --tools exec,read: restrict which tools the job can use

Delivery and output

ModeWhat happens
announceDeliver summary to target channel (default for isolated)
webhookPOST finished event payload to a URL
noneInternal only, no delivery
Use --announce --channel telegram --to "-1001234567890" for channel delivery. For Telegram forum topics, use -1001234567890:topic:123. Slack/Discord/Mattermost targets should use explicit prefixes (channel:<id>, user:<id>).

CLI examples

One-shot reminder (main session):
openclaw cron add \
  --name "Calendar check" \
  --at "20m" \
  --session main \
  --system-event "Next heartbeat: check calendar." \
  --wake now
Recurring isolated job with delivery:
openclaw cron add \
  --name "Morning brief" \
  --cron "0 7 * * *" \
  --tz "America/Los_Angeles" \
  --session isolated \
  --message "Summarize overnight updates." \
  --announce \
  --channel slack \
  --to "channel:C1234567890"
Isolated job with model and thinking override:
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 \
  --announce

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.

POST /hooks/wake

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 (required): event description
  • mode (optional): now (default) or next-heartbeat

POST /hooks/agent

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.2-mini"}'
Fields: message (required), name, agentId, wakeMode, deliver, channel, to, model, thinking, timeoutSeconds.

Mapped hooks (POST /hooks/<name>)

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.

Security

  • Keep hook endpoints behind loopback, tailnet, or trusted reverse proxy.
  • Use a dedicated hook token; do not reuse gateway auth tokens.
  • Set hooks.allowedAgentIds to limit explicit agentId routing.
  • Keep hooks.allowRequestSessionKey=false unless you require caller-selected sessions.
  • 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 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
  1. 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
  1. 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

# 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

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 },
  },
}
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. Maintenance: 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

Cron not firing

  • 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 called without --force.

Cron fired but no delivery

  • Delivery mode is none means no external message is expected.
  • Delivery target missing/invalid (channel/to) means outbound was skipped.
  • Channel auth errors (unauthorized, Forbidden) mean delivery was blocked by credentials.

Timezone gotchas

  • Cron without --tz uses the gateway host timezone.
  • at schedules without timezone are treated as UTC.
  • Heartbeat activeHours uses configured timezone resolution.