Skip to main content
Automations are OpenClaw’s built-in scheduler. The scheduler persists jobs, wakes the agent at the right time, and can deliver output to a chat channel, a webhook, or nowhere. Manage automations with the openclaw automations CLI; openclaw cron remains an alias for the same commands.

Quick start

1

Add a one-shot reminder

2

Check your jobs

3

See run history

How automations work

  • Automations run inside the Gateway process, not inside the model. The Gateway must be running for schedules to fire.
  • Job definitions, runtime state, and run history persist in OpenClaw’s shared SQLite state database, so restarts do not lose schedules.
  • Every automation run creates a background task record.
  • One-shot jobs (--at) auto-delete after successful completion: delivery is confirmed, not requested, intentionally suppressed, or explicitly best-effort. Failed or unknown required delivery retains the job disabled for inspection without replaying the payload. Pass --keep-after-run to keep successful jobs too.
  • Per-run wall-clock budget: --timeout-seconds when set. Otherwise, isolated/detached agent-turn jobs are bounded by the scheduler’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, and script payloads default to 5 minutes.
  • On Gateway startup, overdue isolated agent-turn jobs are rescheduled instead of replayed immediately, keeping model/tool bootstrap work out of the channel-connect window. Startup catch-up delays survive label or payload-content reconciliation and another restart; changing the schedule starts a new scheduling decision.
  • If you drive 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 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.
  • Isolated runs best-effort close tracked browser tabs/processes for their cron:<jobId> session on completion, and dispose any bundled MCP runtime instances created for the job through the same shared teardown path used by main-session and custom-session runs. Cleanup failures are ignored so the run result still wins.
  • Isolated runs with the narrow automation self-cleanup grant can read scheduler status, a self-filtered list containing only their own job, and that job’s run history, and may remove only their own job.
  • Isolated runs guard against stale acknowledgement replies: if the first result is only an interim status update (on it, pulling everything together, and similar hints) and no descendant subagent is still responsible for the final answer, OpenClaw re-prompts once for the actual result before delivery.
  • Structured execution-denial metadata (including node-host UNAVAILABLE wrappers whose nested error starts with SYSTEM_RUN_DENIED or INVALID_REQUEST) is recognized so a blocked command is not reported as a green run, while ordinary assistant prose is not mistaken for a denial.
  • Run-level agent failures count as job errors even with no reply payload, so model/provider failures increment error counters and trigger failure notifications instead of clearing the job as successful.
  • When a job hits timeoutSeconds, the scheduler aborts the run and gives it a short cleanup window. If it does not drain, Gateway-owned cleanup force-clears that run’s session ownership before the scheduler records the timeout, so queued chat work is not stuck behind a stale processing session.
  • Setup/startup stalls get a phase-specific timeout (for example cron: isolated agent setup timed out before runner start or cron: isolated agent run stalled before execution start (last phase: context-engine)). These watchdogs cover embedded and CLI-backed providers even before their external CLI process starts, and are capped independently of long timeoutSeconds values so cold-start/auth/context failures surface quickly.
Automation task reconciliation is runtime-owned first, durable-history-backed second: an active automation task stays live while the automations runtime still tracks that job as running, even if an old child session row still exists. Once the runtime stops owning the job and a 5-minute grace window expires, maintenance checks persisted run logs and job state for the matching cron:<jobId>:<startedAt> run. A terminal result there finalizes the task ledger; otherwise Gateway-owned maintenance can mark the task lost. Offline CLI audit can recover from durable history, but its own empty in-process active-job set is not proof a Gateway-owned run is gone.Restart recovery matches finalized results to the run identity, never just a coincident start time. A verified live process keeps its run receipt. If a foreign process exists but its start identity cannot be verified, its receipt becomes recoverable after more than two hours from the queued or running start. Recovery revokes that receipt before admitting another run; it cannot undo external side effects already in flight. On Gateway startup, an enabled one-shot interrupted before a terminal task result recovers through normal missed-job catch-up, regardless of how overdue it is. Reclaiming a dead running owner during normal operation records the interruption without replaying the consumed one-shot; a separately rescheduled occurrence remains eligible. Catch-up limits and delays pace recovery; they do not expire it. Pending recovery survives another restart, including during agent-turn deferral. A terminal result is restored without replaying that run, and deleteAfterRun deletes the job only when completion is succeeded.

Schedule types

These schedule flags work with both openclaw automations add and openclaw automations edit <job-id>. For example, openclaw automations edit <job-id> --on-exit "./watch.sh" --on-exit-cwd /srv/app converts an existing job to an exit-triggered schedule. 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).

Heartbeat task migration

Older heartbeat scratch supported a structured tasks: block. Run openclaw doctor --fix after upgrading to convert each entry into an ordinary editable main-session automation job. Doctor preserves the interval and previous last-run timing, creates the jobs before removing the block, and safely converges the same declaration keys on rerun. These migrated jobs carry public systemEvent payloads, so openclaw automations list, get, edit, and remove plus the automations agent tool manage them like other jobs (the tool still accepts its legacy cron name as a compatibility alias). Their execution uses the guarded heartbeat task wake: active hours, minimum spacing, flood control, and busy retries still apply, while the scheduler owns each task’s independent cadence. Jobs due in the same coalescing window can share one heartbeat turn. A scheduled occurrence outside heartbeat active hours is skipped and retried at the job’s next occurrence. Heartbeat scratch is now monitor prose only. Runtime heartbeats do not parse tasks: text as schedules; create new recurring work as automations.

Stream sources

A stream schedule keeps an operator-authored argv command running under the Gateway and fires the job from its stdout and stderr lines. Stream schedules are event-driven, never time-due, and are available by default. Set cron.triggers.enabled: false to disable them together with condition-trigger scripts and script payloads. Disabling or removing the job stops the process; Gateway shutdown waits for process-tree teardown. Fast failures restart with the scheduler’s built-in error backoff. Five consecutive runs shorter than 60 seconds leave the job in an error state and use the normal failure-alert path; manually re-enable the job to clear the restart cap.
mode: "line" (the default) accepts every line. mode: "match" accepts only lines matching the compiled match regex. A batch closes after batchMs of quiet (default 250 ms, clamped to 50–5000) or at maxBatchBytes (default 16384, clamped to 1024–65536). At the byte cap the batch ends with [truncated]. Match mode always evaluates complete lines against their full text, even past maxBatchBytes (only the delivered batch is truncated); a line cut at the bounded raw-intake limit is only a prefix, so it is treated as unmatched rather than letting an end-anchored pattern fire on the cut. The batch is appended to the system-event text or agent-turn message. Command payloads are rejected for stream schedules because the source command and payload command would have ambiguous process ownership. Only one payload fire and one bounded pending batch are retained per job. Lines arriving while a payload runs, or before the built-in 30-second trigger interval has elapsed, coalesce into that pending batch rather than building an unbounded queue. One serialized owner records gate drops, payload errors, and not-running dispatches in streamDroppedBatches; bounded merges increment streamCoalescedBatches. Failed payloads are not retried because they may not be idempotent. A logical source identity remains stable across supervised child restarts, but rotates when the source is disabled, removed, or replaced, so queued batches from the retired source cannot fire even after an A-to-B-to-A edit. After a stop completes, late callbacks from an old child are inert. V1 does not include a native WebSocket source; bridge one with an argv command such as websocat wss://example.invalid/events. When a stream job also has trigger.script, the gate runs once per closed batch. The current batch is available as the deeply frozen trigger.streamBatch string alongside trigger.state. fire: false drops that batch after persisting gate state. fire: true keeps existing trigger message semantics, then appends the batch to the resulting payload. A stream job may instead use a script payload without a condition gate; that script receives the batch through the same trigger.streamBatch value. Combining a script payload with a condition gate is rejected because both would own the persisted trigger.state slot.

Dynamic cadence (pacing)

Recurring jobs can set pacing.min and/or pacing.max to duration strings such as 15m or 4h; at least one bound is required. Use --pacing-min and --pacing-max with automations add|edit (--clear-pacing removes both bounds). During an agent-turn run, a paced job can call the automations tool with action: "next_check" and in: "30m". The proposal applies only to that currently running job and is measured from successful run completion. OpenClaw silently clamps it to the configured bounds. Pacing without a proposal leaves the normal schedule unchanged. Failed, timed-out, and skipped runs discard the proposal, so existing retry and error-backoff behavior takes precedence. Manually forcing a recurring job is out-of-band and preserves its pending natural or paced slot. For condition-triggered jobs, the built-in minimum interval remains a lower bound even when a proposal requests an earlier check.

/loop chat shortcut

In chat, the owner-only /loop [interval] <prompt> command creates a recurring agent-turn job bound to that conversation. Give an interval such as 5m for fixed cadence, or omit it to let the loop self-pace between 1 minute and 1 hour with next_check. Use /loop status to list conversation-bound loops and /loop stop [name] to remove them.

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

Event triggers (condition watchers)

An event trigger adds a headless condition script to an every, cron, or stream schedule. Time schedules evaluate it when due; stream schedules evaluate it for each closed batch. The scheduler runs the normal payload only when the script returns fire: true:
When upgrading, run openclaw doctor --fix to migrate persisted trigger scripts that call tools.call('exec', args) and read the legacy .result.details envelope. Doctor leaves custom or ambiguous legacy scripts unchanged and identifies each affected job for manual conversion; standalone script payloads are not migrated. The script must return { fire, message?, state? }. The previous JSON state is available as the deeply frozen trigger.state; stream gates also receive the current batch as trigger.streamBatch. Return a new state value to persist it. State is capped at 16 KB. When a firing result includes message, the scheduler 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 built-in minimum interval of 30 seconds. Each evaluation has a 30-second wall-clock budget and up to 5 tool calls. Removing or disabling a job during condition evaluation cancels that evaluation before its payload can start. After a main-session payload hands work to heartbeat, that shared heartbeat retains its own lifecycle. Author watchers around actionable state, not only success: a watcher that goes quiet when its check fails or times out looks healthy while broken. Compare the observation with trigger.state and return fresh state to deduplicate; do not rely on model or process memory. When firing, make message self-contained because it becomes the fired run’s complete event context.
Condition-trigger scripts and script payloads run unattended by default with the owning agent’s full tool policy, including exec. Stream schedules also keep operator-authored commands running unattended. Treat these surfaces as unattended code execution with that agent’s permissions. Operators who need a hard stop can set cron.triggers.enabled: false; remove it or set it to true to re-enable them.
Create a watcher from a local script file (- reads the script from stdin):

Promoting a repeated job into an automation

Most automations should start as work the agent already did. When you ask for substantially the same job several times, the agent offers to turn it into a schedule instead of only running it once more. Promotion is preferred over building a job from scratch because the proposal inherits a run you already read: you know what the output looks like before it starts arriving on a schedule. There is no repetition-detection engine and no new stored history. The agent recognizes the repeat from the conversation itself and checks automations(action: "list") for an existing job before proposing a new one, so a routine you already created is not duplicated. The prompting that drives this is gated on the automations tool, so agents without it never offer a routine they could not create. The confirmation restates the schedule and the task in plain words before anything is created, for example: “Every weekday at 07:00 Europe/Vienna, I summarize overnight updates and post them here.” Confirm that sentence, not a cron expression. On confirmation the agent:
  1. Creates the job, with delivery defaulting to the channel and thread where you asked.
  2. Immediately runs it once with run in force mode as a visible test, delivered to that same thread, so you see real output well before the first scheduled occurrence.
  3. Removes the job and tells you if that test fails.
The job is created enabled, not disabled-pending-approval, and that is a deliberate safety choice. The scheduler supervises enabled jobs: a failing one raises a failure notification and is auto-disabled after repeated errors, with the reason recorded and the owner notified. Nothing supervises a disabled job. A job left disabled waiting for a confirmation that never arrives is invisible to every guard, hidden from the default automations list, and will never fire or explain itself — a silent non-outcome, which is a worse failure than a job that runs and visibly complains. Your confirmation still gates creation, so nothing is scheduled behind your back, and the test run is a real run with real delivery rather than a rendered preview: what you approve is exactly what the schedule will produce.

Payloads

Every job carries exactly one payload kind, chosen by flag: System-owned payload kinds are gateway-converged and cannot be created or edited through the CLI or API. The heartbeat kind creates one heartbeat monitor job per heartbeat-enabled agent (see Heartbeat). The skillCollectionReview kind creates one Skill Workshop review job per writable workspace. Both appear in openclaw cron list; use --all to include disabled rows. Skill collection review runs every 7 days. It is enabled when skills.workshop.autonomous.mode is auto; propose and off keep the system-owned job disabled. The Gateway converges these jobs at startup and after config reload. Scheduled reviews require automations. When cron.enabled is false or OPENCLAW_SKIP_CRON=1, the Gateway logs a startup warning and does not run scheduled reviews. There is no separate weekly Gateway timer.

Agent-turn options

string
required
Prompt text (required for isolated/current/custom-session jobs).
string
Model override; must resolve to an allowed model or the run fails with a validation error.
string
Per-job fallback model list, for example --fallbacks openai/gpt-5.6-sol,openrouter/meta-llama/llama-3.3-70b-instruct:free. Pass --fallbacks "" for a strict run with no fallbacks.
boolean
On automations edit, removes the per-job fallback override so the job follows configured fallback precedence. Cannot combine with --fallbacks.
boolean
On automations edit, removes the per-job model override so the job follows normal automation model precedence (stored automation-session override, else agent/default model). Cannot combine with --model.
string
Thinking level override (off|minimal|low|medium|high|xhigh|adaptive|max|ultra). Available levels still depend on the selected model and agent runtime.
boolean
On automations edit, removes the per-job thinking override. Cannot combine with --thinking.
boolean
Skip workspace bootstrap file injection.
string
Restrict which tools the job can use, for example --tools exec,read.
New jobs that can run tools always store an explicit tool policy. Jobs created by an agent are capped to the tools available to that creating turn, and the agent cannot widen the stored list. Jobs created by an authenticated operator without --tools store an unrestricted * policy; automations edit --clear-tools restores that explicit unrestricted policy. Existing jobs that predate an explicit tool policy retain their current behavior until their tool policy is explicitly edited or the job is recreated. --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. Pick the model for the job’s difficulty, not the agent’s default. Routine automation - summaries, triage, classification, status checks - runs well on a lighter model, which is cheaper and faster per run and adds up across a schedule. Keep your default model for jobs that need deep reasoning, and use --fallbacks when a light primary should escalate on failure. Model-selection precedence for isolated jobs, highest first:
  1. Per-job payload model (explicit config; a disallowed model fails the run)
  2. Gmail hook model override (only when the run came from Gmail and that override is allowed)
  3. User-selected stored automation-session model override
  4. Agent/default model selection
Fast mode follows the resolved live selection. Isolated automation resolves it in this order: stored session fastMode, per-agent agents.entries.*.fastModeDefault, global agents.defaults.fastModeDefault, then selected-model params.fastMode. Auto mode uses the model’s params.fastAutoOnSeconds cutoff, defaulting to 60 seconds. If a run hits a live model-switch handoff, the scheduler 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, the scheduler 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

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 the job’s run history, and reuse the same announce, webhook, and none delivery modes as agent-turn jobs.
When an agent-turn automation’s exec needs approval, the card is delivered to connected approval surfaces and the run waits for the decision; answering Always allow mints a scoped standing grant so later occurrences run without prompting. See Standing grants for automations for lifetime, listing, and revocation.Command payloads are an operator-admin Gateway automation surface, not an agent tools.exec call. Creating, updating, removing, or manually running automation jobs requires operator.admin; scheduled command runs later execute inside the Gateway process as that admin-authored automation. Agent exec policy (tools.exec.mode, approval prompts, per-agent tool allowlists) governs model-visible exec tools, not command payloads.
--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, the scheduler 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 automation silent-token suppression and posts nothing back to chat.

Script payloads

Script payloads run headlessly in the same code-mode executor as trigger scripts, without starting a conversational agent turn. They are available by default; setting cron.triggers.enabled: false disables creation and execution of script payloads together with condition-trigger scripts and stream schedules. Script jobs support only main and isolated session targets.
Use --script <file|-> to read JavaScript from a file or stdin. The timeout defaults to 300 seconds and is capped at 900; the tool budget defaults to 50 calls and is capped at 200. These payload budgets are separate from the smaller trigger-gate evaluation budgets. The script may return an object with these optional fields:
  • notify: Text delivered through the job’s announce, webhook, or none delivery mode. If omitted, nothing is delivered. For a main job, the text becomes a system event.
  • wake: "now" requests an immediate heartbeat after enqueueing notify (or a compact completion event); "next-heartbeat" enqueues the event for the next heartbeat.
  • state: JSON state, capped at 16 KB and persisted only after a successful run. The next run receives a frozen copy as trigger.state, matching trigger scripts. Because that namespace has one persisted owner, a script payload cannot be combined with a condition trigger on the same job.
  • nextCheck: A duration such as "15m". It is valid only for jobs with pacing enabled and uses the same pacing clamp as agent-turn proposals.
Throws, timeouts, exhausted tool budgets, invalid results, and nextCheck without pacing are normal automation run errors: they enter run history, backoff, and failure-alert handling without persisting returned state.

Execution styles

Codex apps in scheduled automations

Codex-created automations can retain the app IDs and permission ceiling available to the authenticated creator thread. At execution, OpenClaw requires the same prepared Codex profile and account, then narrows the stored cap against current app policy. Revoked apps, account/runtime changes, and interactive approval requirements fail closed with a recovery message; they never fall back to broader or different credentials. Older jobs without a captured app envelope continue their ordinary non-app behavior; recreate or reauthorize one only when it needs Codex app access. See Native Codex plugins. Agent-turn jobs default to the creating conversation when the create request carries session context. Callers without a session key, including CLI and API callers that do not supply one, fall back to isolated. System events and heartbeats still default to main; command and script payloads still default to isolated.
Main session jobs enqueue a system event into the owning agent’s main session and optionally wake the heartbeat (--wake now or --wake next-heartbeat). The event is processed with that session’s existing context and last delivery context. Internal automation turns do not extend daily or idle reset freshness; only visible user activity updates session freshness. Current-session jobs execute in a detached run session, read a bounded tail of the conversation captured when the job was created, and commit the final visible assistant result back to that exact conversation. 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.Main-session automation events are self-contained system-event reminders. They do not automatically include the default heartbeat prompt or the heartbeat monitor scratch; say it explicitly in the automation event text if a reminder should consult that context.Main-session jobs use the owning session’s delivery context, not a separate chat announce target. Edits that enable announce delivery, or set a chat target without explicitly choosing no delivery, are rejected without changing the job. Use an isolated job with --message and --announce for chat delivery. Primary webhook delivery remains supported for main-session jobs.
A new transcript/session id per run. OpenClaw carries safe preferences (thinking/fast/verbose settings, labels, explicit user-selected model/auth overrides), but does not inherit ambient conversation context from an older automation session 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.
Isolated automation and hook agent turns are explicitly unattended: no one is present to clarify or approve. The final reply must be the deliverable rather than a plan, acknowledgement, or request for input. The agent returns NO_REPLY when nothing needs doing and states failures plainly; the scheduler owns retry and failure-alert policy.For trusted scheduled jobs, the job’s own instructions win when they intentionally ask for a question or plan, and the agent may remove a job that is no longer needed. External hook turns receive only the common unattended contract; they do not receive that override or self-removal guidance across the external-content boundary.
When isolated automation runs orchestrate subagents, delivery 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 and the final answer. Media and structured Discord payloads are still delivered separately so attachments and components are not dropped.

Delivery and output

A successful primary webhook run with no nonblank summary intentionally skips the POST and records deliverySuppressionReason: "empty", matching announce delivery’s optional-output contract. Execution errors still send the error event even without a summary. When gateway.publicOrigin is configured and the Control UI is enabled, chat notifications include an Inspect link into the Control UI. Command and script completion announcements open the automation run; isolated agent announcements open the run’s session. For a current job using announce (the default), the final assistant result is a first-class session completion, not a WebChat-specific outbound message. OpenClaw waits for active turns in the creation-bound conversation, verifies that the same session generation still owns the key, and commits the result through the canonical transcript writer with cron job/run provenance and a job/run idempotency key. A retry cannot append the same result twice. WebChat receives the committed session.message event immediately. The same assistant result comes from chat.history after a refresh or reconnect; no follow-up user message is required. Delivery is successful only after that transcript/event commit succeeds. If the bound conversation is an external channel, OpenClaw also performs its normal durable channel send. That send still happens at most once, and the required session commit does not create a second external message. A verified message tool send suppresses the automatic channel resend but does not suppress the session commit. The run is reported delivered only after both the external recipient handoff (when required) and the canonical session commit succeed. When the bound conversation has no external channel route — WebChat/Control UI conversations, or a gateway with no channel plugins configured — the session commit alone completes delivery and the run succeeds without attempting an external send. If the conversation does name an external route that cannot be resolved at run time, the committed result stays in the conversation and the run records the resolution failure as its delivery error: a delivery failure, not a turn failure.
Every outbound automation webhook uses the strict SSRF guard. Loopback, private/internal, link-local, and other special-use targets are refused by default for primary delivery, completion and failure destinations, and failure-alert webhooks.Allow only the receiver you trust with an exact hostname or IP exemption:
Use dangerouslyAllowPrivateNetwork: true under webhookSsrfPolicy only when every configured automation webhook may reach trusted private-network services. Leaving the policy unset keeps strict behavior.
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. On hosts with multiple configured channels, isolated announce jobs created with automations add|create or changed with automations edit must set --channel <channel-plugin-id> unless a provider-prefixed --to or a preserved session route selects the channel. Use --best-effort-deliver only when unresolved fallback delivery is acceptable; it does not choose a channel, and a delivery failure does not fail the job. Channel announcements retry transient failures only when no payload may have reached the recipient. A successful retry records delivery without retaining the earlier attempt’s error, including with best-effort delivery. Partial or ambiguous sends are not replayed by the announcement retry loop. When announce delivery uses channel: "last" or omits channel, a provider-prefixed target such as telegram:123 can select the channel before the scheduler 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

Execution failures use one scheduler-owned threshold and cooldown policy. A job with an existing failure route is covered by default after 2 consecutive failures with a 1-hour cooldown. The route can be a resolved failure destination or the job’s primary announce target. Jobs with no such route stay quiet unless a per-job or global failureAlert object explicitly activates the policy. Failure notification routes resolve in this order:
  1. Route fields in the job’s failureAlert object.
  2. job.delivery.failureDestination, layered over the destination fields in global cron.failureAlert (mode, channel, to, accountId). The retired cron.failureDestination block is merged into the global object by openclaw doctor --fix.
  3. The job’s primary announce target.
  • job.failureAlert: false disables execution and required-delivery failure alerts for that job. The auto-disable safety notification remains active.
  • Global cron.failureAlert.enabled: false disables inherited notifications. A per-job failureAlert object explicitly re-enables that job; enabled: true explicitly enables the global policy.
  • A per-job failureAlert object or any global cron.failureAlert object activates and tunes the policy even when the job had no existing route.
  • delivery.bestEffort: true suppresses inherited/default execution-failure alerts. An explicit per-job failureAlert remains authoritative.
  • delivery.failureDestination is only supported on sessionTarget="isolated" jobs unless the primary delivery mode is webhook.
  • failureAlert.includeSkipped: true opts a job or global automation alert policy into repeated skipped-run alerts. Skipped runs keep a separate consecutive-skip counter, so they do not affect execution-error backoff.
  • openclaw automations 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.
In the Control UI, custom failure alerts show stored threshold, cooldown, and mode overrides. An omitted channel displays the neutral last choice without storing it. Leave the threshold or cooldown blank, or choose Inherit global setting for alert mode, to use the Gateway’s normal global and routing defaults. Cooldowns accept decimal seconds with millisecond precision, including 0 for no cooldown; for example, 1.001 seconds preserves 1001 milliseconds. Editing other job fields or cloning a job preserves its alert policy, including the skipped-run setting. A required completion-delivery failure is distinct from an execution failure: a run can record status: "ok" with completionStatus: "failed". It does not increment the execution-failure streak or backoff. A delivery-failure alert can notify through a resolved alternate failure destination without waiting for failureAlert.after. All such alerts, including the first delivery failure after an execution alert, honor the shared job/global failureAlert.cooldownMs (default 1 hour); suppressed alerts still leave the delivery failure in run history. Skipped runs and quiet trigger checks do not clear the cooldown; successful completion does. The scheduler never retries the already-failed primary route for an alert. Chat failure notifications include the run start time in the agent’s configured user timezone. When gateway.publicOrigin is configured and the Control UI is enabled, they also include an Inspect link to the automation run. Webhook message text stays stable; integrations can read the same instant from the structured runAtMs field and construct their own links. Chat notifications show normalized failure causes or allowlisted producer facts for known command and script failures. Arbitrary commands, paths, provider bodies, secrets, delivery errors, skip reasons, diagnostics, and stack/error text remain in automation history. Failure webhooks retain the structured raw error for diagnostic integrations. The scheduler also provides an unconditional safety backstop. A time-based recurring job is auto-disabled after 10 consecutive execution failures; a successful run resets that streak. On the terminal failure, the richer auto-disable notification replaces the regular threshold alert. Repeated schedule-computation failures auto-disable after 3 errors. The job records state.autoDisabled.reason as consecutive-failures or schedule-errors, and the owning agent receives a notification with a safe cause and recovery command. Raw errors stay in automation history. After fixing the cause, run openclaw automations enable <jobId>; enabling clears the recorded reason and failure streaks. Because disabled jobs are hidden by the default list, use openclaw automations list --all to inspect them.

Output language

Automation jobs do not infer a reply language from channel, locale, or previous messages. Put the language rule in the scheduled message or template:
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.”

CLI examples

Managing jobs

Conversational management

In the authenticated Control UI, an administrator with operator.admin can ask the agent to list, inspect, update, run, or remove any existing automation on that Gateway, regardless of its creator or channel. For example, ask it to disable a reminder created in Telegram. This matches the administrator’s authority on the Automations page. Create command payloads through the operator CLI or Gateway API. The Gateway grants this authority from the authenticated Control UI turn’s admission facts. Each operation uses a one-use grant that expires after 60 seconds and remains bound to that exact active run. Channel turns and Control UI turns without operator.admin receive no such grant; matching sender IDs, account IDs, or session routes never establish it. If access is denied or a grant expires, retry from a fresh authenticated Control UI administrator turn, or use the Automations page. Each admin management request records its method, run, operational instance, and success or failure in the Gateway’s cron: admin management log, alongside the ordinary tool audit record. Management authority does not transfer creator attribution or replace the job’s scheduled execution policy.

CLI management

Archiving a session (Control UI, or sessions.patch { key, archived: true, expectedSessionId } using the durable ID from sessions.list) disables every enabled automation job bound to that session: its isolated cron:<jobId> session, a session:<key> target, or a delivery/wake sessionKey lane. Restoring the session requires the same observed identity and does not re-enable those jobs; use openclaw automations enable <jobId>. Sessions with an enabled bound job show a clock badge in the Control UI sidebar. openclaw automations 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 only for completionStatus: "succeeded". Failed or unknown completion and wait timeouts exit non-zero. Run history keeps payload execution in status (ok, error, or skipped) and whole-run completion in completionStatus (succeeded, failed, or unknown). Requested delivery is required unless the admitted job explicitly sets delivery.bestEffort: true; delivery-only failure leaves execution status: "ok", does not increment execution error counters or enter retry backoff, and records completionStatus: "failed". An adapter send without a delivery identity stays unknown, without an automatic resend that could duplicate the message. Intentional silence (NO_REPLY), intentionally empty output, heartbeat acknowledgments, and channel reply transforms record deliverySuppressionReason without claiming delivery or triggering delivery-failure alerts. These successful non-outcomes and successful executions with explicit delivery.bestEffort: true delete one-shots normally. A transport hook veto instead records a delivery error without an intentional-suppression reason. Active descendants without a final reply, stale interim output, and output emptied by TTS instead record a delivery error. Retained one-shot jobs do not automatically rerun; inspect their history and delivery outcome before retrying or removing them. Direct Gateway event sources can use cron.run with mode: "if-enabled" to run immediately without overriding an operator-disabled or auto-disabled job. Explicit operator run-now commands continue to use force. The agent automations tool returns compact job summaries (id, name, enabled, nextRunAtMs, scheduleKind, lastRunStatus) from automations(action: "list"); use automations(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. cron.add includes the same dry-run preview on the created job so create-time output names a resolved route or fail-closed outcome. openclaw automations create is an alias for openclaw automations 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 automations add|create or automations 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 automations 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. The webhook URL remains subject to the strict outbound policy above; configure cron.webhookSsrfPolicy for an intentional local or private receiver.
Model override note:
  • openclaw automations 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, the scheduler fails the run with an explicit validation error.
  • API cron.update payload patches can set model: null to clear a stored job model override.
  • openclaw automations edit <job-id> --clear-model clears that override from the CLI (same effect as the model: null patch) and cannot combine with --model.
  • Configured fallback chains still apply because the automation --model is a job primary, not a session /model override.
  • openclaw automations add|edit --fallbacks ... sets payload fallbacks, replacing configured fallbacks for that job; --fallbacks "" disables fallback and makes the run strict. openclaw automations edit <job-id> --clear-fallbacks clears the per-job override.
  • A plain --model with no explicit or configured fallback list does not fall through to the agent primary as a silent extra retry target.

Webhooks

Gateway HTTP hooks let an external service wake an agent or submit an agent turn. They are disabled by default. These endpoints are separate from internal event hooks (HOOK.md handlers) and the Webhooks plugin, which manages TaskFlow records. They also differ from outbound automation webhook delivery: here, the external service calls OpenClaw.

Enable and test an agent hook

Start with a running Gateway and an agent that can complete a normal turn. Merge this into your config, replacing the token with a long random value and main with the intended configured agent:
Use a token dedicated to hooks, not the Gateway auth token or password. Run these commands on the Gateway host with its profile/config. Validate the configuration, restart the installed service to load it, and watch the logs:
If you run the Gateway in the foreground rather than as an installed service, stop and start that process instead. In another terminal, send a harmless test to the local Gateway. Replace the token, agent id, and port to match your configuration:
The expected admission response is HTTP 200:
This means the run acquired session/global placement admission. It does not mean the model finished, a tool succeeded, or a message was delivered. A single agent request can wait up to 15 seconds for admission; the model runtime may still be preparing when the response arrives. In openclaw logs --follow, search for hook agent run completed and the exact HTTP runId. Runs with status=ok and no explicit delivery error log at info level; all non-ok statuses (including skipped runs), thrown errors, and explicit delivery errors log at warn level. For this deliver: false test, expect status=ok with no successful announcement. A warning with status=ok and deliveryError means execution succeeded but delivery failed. It does not trigger another announcement attempt. Structured terminal records include the accepted agentId, jobId, hook name and source path, and logicalSessionKey. When the runner returns them, sessionId correlates the run transcript and sessionKey identifies the runtime session key. Exact-run continuation aliases can be retired after completion; the key does not guarantee a separate durable session row. Missing session facts remain unknown. Diagnostics are redacted, single-line, and bounded to 500 characters per string. Successful output is not logged: inspect the agent’s run session for it. The HTTP runId correlates hook logs; it is not a TaskFlow id or a task id to pass to openclaw tasks show. sessionMode defaults to isolated, so this test gets a fresh run session and a generated logical hook:<uuid> key. The stored session can use a cron:...:run:... key; the logical hook key is not a promise about the transcript’s storage key. A fixed defaultSessionKey serializes requests sharing that key, even in isolated mode; use it only when that ordering is intended.

Authentication

Every request must include the hook token via one of these headers:
  • Authorization: Bearer <token> (recommended).
  • x-openclaw-token: <token>.
Query-string ?token=... authentication is rejected. Send JSON with Content-Type: application/json. All hook endpoints accept POST only. The Hooks reference lists payload fields, limits, routing policy, and error responses.
Enqueue a trusted notification for the selected agent’s main session and optionally request an immediate heartbeat:
HTTP 200 includes eventOutcome: "queued" when the queue accepts the wake or eventOutcome: "coalesced" when the same wake is already the queue’s most recent pending event. With mode: "now", a wake is requested in either case; the response does not mean a heartbeat completed. Use mode: "next-heartbeat" to avoid requesting an immediate wake.A supplied agentId must name a configured agent. Supply it explicitly when the fleet has no implicit or retained legacy owner. A caller-selected sessionKey requires mode: "now", hooks.allowRequestSessionKey: true, and the configured prefix policy; deferred wakes use the main session.Wake text is a system event, not an isolated, safety-wrapped email reader turn. Send only a short notification you control. Route raw email, documents, or other untrusted content through an agent action with a restricted reader.
Submit an agent turn with a required message. Optional routing, model, thinking, timeout, and idempotency fields are documented in the payload reference.Keep sessionMode: "isolated" for fresh context. Set "persistent" only when repeated events should reuse prior context: direct requests then require an explicit sessionKey, hooks.allowRequestSessionKey: true, and nonempty hooks.allowedSessionKeyPrefixes.For direct channel delivery, supply both a concrete channel and to; add accountId to select an enabled channel account. Supplying only part of a destination, using channel: "last", or selecting an invalid account returns 400 before dispatch. Direct hooks do not inherit the main session’s last recipient.With no destination, the default deliver: true allows a completion system event on the target agent’s main session. Set deliver: false to suppress successful announcements and ignore destination fields; completion is logged instead. Non-ok outcomes still produce a failure event. Disabling announcement is not a tool restriction: restrict the agent’s tools separately if it must not send messages.
Custom paths resolve through hooks.mappings. The first matching mapping wins, ahead of presets. Templates or trusted local JS/TS transforms turn the payload into wake or agent actions; a transform returning null produces HTTP 204 without a run. See Mapping details.Persistent mapped hooks require a stable mapping sessionKey or hooks.defaultSessionKey. Template-derived keys require the same caller-key opt-in and prefix policy as request keys.forEach: "<key>" fans out over a top-level payload array. Each item sees a one-element array, so the Gmail preset’s messages[0] means the current email. Agent fan-out admission answers after at most about 8 seconds of dispatch waiting; pending items continue in the background and a partial batch returns non-2xx. Retrying the same batch reuses pending or admitted agent items while the bounded in-memory replay cache retains them. It is not durable exactly-once delivery; mapped wake actions have no replay identity, and the queue may coalesce repeated wakes. The reference covers batch caps and response shapes.

Verify and troubleshoot hook requests

For delivery-enabled requests, also verify receipt at the intended channel, account, and recipient. Check terminal warnings for deliveryError, including when status=ok. delivered: false alone does not prove failure, and deliveryAttempted: true does not prove receipt. Explicit suppression and message-tool delivery can already satisfy the runner’s delivery handling; missing delivery flags remain unknown. For retried agent requests, reuse an Idempotency-Key and the same payload. The reference explains its scope and lifetime. Use a new key for a new test; a replayed 200 does not run the agent again.
Keep endpoints behind loopback, a tailnet, or a trusted reverse proxy. Use HTTPS for remote calls and expose only the required path.
  • Use a dedicated hook token and a dedicated subpath; / is rejected.
  • Restrict hooks.allowedAgentIds, including the effective default-agent path.
  • Keep hooks.allowRequestSessionKey: false unless required; when enabled, constrain hooks.allowedSessionKeyPrefixes.
  • Treat external event content as data. Agent hook content is safety-wrapped by default, but wrapping does not remove tools or workspace access. Use a restricted agent for untrusted inputs and keep unsafe-content overrides disabled.

Gmail PubSub integration

Wire Gmail inbox triggers to OpenClaw through Google Pub/Sub and gog gmail watch serve. Pub/Sub calls the watcher; the watcher forwards email data to the Gateway HTTP hook. This does not load or invoke an internal HOOK.md handler. Not on Gmail? The IMAP email trigger plugin watches an existing IMAP mailbox without Google PubSub or a public webhook.
Prerequisites: gcloud CLI, gog (gogcli) authorized for the watched Gmail account, OpenClaw hooks enabled, an HTTPS push endpoint reachable by Pub/Sub (Tailscale Funnel in the recommended setup), and a working sandbox backend. The example below uses the default Docker backend; build its image first by following Sandbox images and setup, or configure another supported backend.
Before connecting Gmail transport, merge a dedicated reader and hook policy into your existing config. Preserve the real settings on your existing agent; the main entry below only shows the required roster shape.
Adding mail_reader creates an explicit fleet. Keep existing bindings and add one channel-wide binding per enabled channel that main still owns; there is no cross-channel wildcard.
Before restart, run openclaw agents list --bindings; replace every placeholder and verify each channel owner. Why this shape is safer:
  • The explicit main binding preserves existing channel ownership instead of leaving non-Gmail traffic ownerless. Use a specific accountId instead of "*" when only one account belongs to main.
  • agentId: "mail_reader" keeps Gmail off the main agent.
  • allowedAgentIds prevents this hook endpoint from selecting another agent. If the Gateway serves other hook workflows, include only their intended agent ids too.
  • scope: "session" gives each Gmail message its own sandbox; workspaceAccess: "none" keeps the host agent workspace out of that sandbox.
  • allow: ["session_status"] is an absolute per-agent clamp, so global tools.alsoAllow additions cannot leak into the reader. The minimal profile and explicit deny list make the intended boundary auditable.
  • deliver: false disables automatic successful announcements; completion is logged instead. To announce a summary externally after validating the reader, set deliver: true and add an explicit channel and to. Agent-to-agent access is on by default: set tools.agentToAgent.enabled: false to disable cross-agent handoff, or deliberately expose the exact coordination tool and constrain permitted agent pairs with tools.agentToAgent.allow.
Tool policies can only become more restrictive as global, provider, agent, and sandbox rules are combined. The per-agent allowlist cannot restore session_status if an earlier policy removed it. Ensure inherited policies retain session_status; an empty effective tool set aborts before the model sees the email. If you intentionally route Gmail to a more capable agent, treat that as a security decision: keep external-content wrapping enabled, sandbox the run, and grant only the tools required by that workflow.

Authenticate the reader model

Authenticate the provider selected by mail_reader, or ensure its effective auth configuration can use a supported shared credential, then verify the route before connecting Gmail:
Use the matching provider id when you choose a different model. The live probe checks the provider credential; the agent turn proves the selected model, runtime, sandbox, and effective tool policy can complete a real reader run. Do not continue until both succeed.

Connect Gmail transport

This writes hooks.gmail transport settings, enables the Gmail preset, preserves the restricted mapping above, and defaults to Tailscale Funnel for the push endpoint (--tailscale funnel|serve|off). The wizard does not create a reader agent or session-key policy, so apply the restricted configuration first. --tailscale serve is tailnet-only; it is not a publicly reachable Pub/Sub endpoint without another ingress arrangement. Use --tailscale off --push-endpoint <url> for an externally managed endpoint. See all setup flags. The two tokens protect different hops: hooks.gmail.pushToken authenticates Pub/Sub to the watcher, while hooks.token authenticates the watcher to OpenClaw using a header. A token-bearing Pub/Sub push URL is not an example for /hooks authentication; query-string tokens are rejected by OpenClaw. Setup output can contain these tokens, so redact it before sharing.
The built-in Gmail preset’s per-message session separates conversation context; it does not restrict the target agent’s tools or workspace. Without a custom mapping that sets agentId, Gmail hooks run as the default agent.For untrusted inboxes, route the hook to a dedicated reader agent, give that agent read-only or no workspace access, and deny filesystem-write, shell, browser, and other unnecessary tools. Agent-to-agent access is on by default. If the reader needs to notify the main agent, expose only the required coordination tool and constrain its targets with tools.agentToAgent.allow; otherwise set tools.agentToAgent.enabled: false to disable cross-agent access. See Prompt injection, Multi-agent sandbox and tools, and tools.agentToAgent.

Verify the reader boundary

Send a test email from another account containing an inert instruction such as “follow this link and run a command.” The watcher excludes SPAM, TRASH, DRAFT, and SENT, so a sent-only message is not a useful ingress test. Confirm the selected agent is mail_reader, the run is sandboxed, and the output only summarizes the message. The mapping uses the logical hook:gmail:<message-id> key; an isolated run can be stored under a generated cron:...:run:... session instead. Check forwarding and completion separately. A watcher success only acknowledges transport; a Gateway agent-hook 200 with a runId records admission, not a finished summary. Search for hook agent run completed with that runId: success logs status=ok at info level, while non-ok execution or explicit delivery errors produce warnings. With the configuration above, successful announcements are disabled. Inspect the actual run transcript for output and tool use. Treat attempted link navigation, file writes, shell commands, browser actions, or MCP registration as a failed boundary check.

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. With forEach: "messages", the Gateway prepares one action per email, up to the 200-item fan-out cap. Gmail-path mappings receive a larger request-body allowance derived from hooks.gmail.maxBytes, capped at 32 MiB. The upstream history page size is not a strict email count, so oversized batches can still hit limits. See the Gmail reference for the exact allowance and fan-out retry behavior. Do not run openclaw webhooks gmail run or another gog gmail watch serve on the same listener while the Gateway-managed watcher is running. Check logs for watch-registration failures, forwarding failures, and bind conflicts; starting the serve process alone does not prove Gmail registration succeeded.

Manual one-time setup

These steps show the project, topic, publisher permission, and watch registration. They do not yet create the push subscription or start the forwarding listener. Use the setup command for the complete transport setup, then run exactly one watcher.
1

Select the GCP project

Select the GCP project that owns the OAuth client used by gog:
2

Create topic and grant Gmail push access

3

Start the watch

Gmail model override

Use the latest-generation, best-tier model available from your provider for untrusted inboxes. The value above is an example; the model must exist in your configured catalog and allowlist.

Configuration

webhookToken is sent as Authorization: Bearer <token> on automation webhook POSTs. Webhook URLs must not include embedded username/password credentials; use webhookToken when the receiver supports bearer authentication. webhookSsrfPolicy applies to every outbound automation webhook and is strict when omitted. Prefer narrow allowedHostnames entries over the broad dangerouslyAllowPrivateNetwork opt-in. Automation jobs, run history, and quarantined malformed jobs live in the shared SQLite state database. Use the CLI or Gateway API to change jobs; cron.store is retired. Set cron.skipMissedJobs: true to skip recurring (cron and every) slots missed while the Gateway was offline. At startup, those jobs advance to their next future occurrence instead of catching up, avoiding stale reminders and unnecessary model calls at the cost of dropping missed work. The default is false (catch up); one-shot (at) jobs retain their normal catch-up behavior either way. Disable automations: cron.enabled: false or OPENCLAW_SKIP_CRON=1.
One-shot retry: transient errors (rate limit, overload, network, timeout, server error) use a built-in retry schedule. Permanent errors disable the job immediately.Recurring retry: consecutive execution errors back off on an extended schedule (30s, 60s, 5m, 15m, 60m). Backoff resets after the next successful run.
cron.sessionRetention (default 24h, false or "0h" disables) prunes isolated run-session entries. Terminal run history is retained for 7 days (lost rows for 24 hours), with the newest 2000 rows per job and history class enforced as an additional ceiling.
On upgrade, run openclaw doctor --fix to import historical ~/.openclaw/cron/jobs.json, jobs-state.json, jobs-quarantine.json, and runs/*.jsonl files into SQLite and archive the originals with a .migrated suffix. Malformed job rows remain recoverable in SQLite while valid jobs keep running.

Troubleshooting

Command ladder

  • Check cron.enabled and the 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 the manual run was checked with openclaw automations 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.
  • When the dispatcher records intentional suppression, job state, run history, and finished events include deliverySuppressionReason (empty, silent, heartbeat, or channel_transform). This is separate from lastDeliveryError / deliveryError; required delivery failures also log an error when they happen.
  • If the isolated run returns only the silent token (NO_REPLY / no_reply), OpenClaw suppresses direct outbound delivery and 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.
  • Automation 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 expressions without --tz use the gateway host timezone.
  • at schedules without timezone are treated as UTC.
  • Heartbeat activeHours uses configured timezone resolution.