Detect agent runtime anomalies and route incidents through approval-safe guardrails. Use when you need deterministic checks for cron failures, context pressu...
集成
Always On Agent Ops
试用Keep an OpenClaw agent running 24/7 — loopback gateway blocks inbound webhooks, OAuth expires silently, strict config. Use when crons stop firing. Trigger on...
它能做什么
Keep an OpenClaw agent running 24/7 — loopback gateway blocks inbound webhooks, OAuth expires silently, strict config. Use when crons stop firing. Trigger on "agent went quiet".
技能文档
OpenClaw Always-On Operations
An always-on agent does not die loudly. It goes quiet — expired auth, an unloaded plugin, a dispatcher pointing at an id that no longer exists. Everything below exists to make silence impossible.
Access, data, and network — read before running anything
| Item | What this skill needs |
|---|---|
| Host access | A logged-in GUI desktop session, permanently. The daemon is not headless. |
| Credentials | A gateway token (loopback only), a provider auth profile, and any queue Bearer you add. All in ~/.openclaw/ or the OS keychain — never in git, never in the agent workspace. |
| Data persisted | Cron job definitions + run state (~/.openclaw/cron/jobs.json), the agent workspace and its memory files, logs. |
| Retention — executed, not declared | Logs: 30 days, deleted by scripts/purge-retention.sh (cr-22, weekly). Orphaned lock dirs: 15 min TTL, reclaimed by the dispatcher. Cron run state: life of the job — it goes when you delete the job. Workspace/memory: no automatic purge — cr-22 reports its size so growth cannot stay invisible; the number you keep is yours to set. |
| Network out | Only what YOU configure: the queue you poll, the notification channel you name. Default is no network — with no secrets.env, the preflight emits Human action required and exits 0. |
| Network in | None. The gateway binds loopback. The optional loopback listener (below) is opt-in and never leaves the host. |
| Notifications | ${NOTIFY_CHANNEL} receives short status lines. They leave the machine. Never put secrets, PII, or payload dumps in them. |
Everything a cron reaches out to is a system you are responsible for: honour its terms of service, and for any personal data touched, apply GDPR — legal basis, minimisation, and a stated retention window — before you schedule the first run.
Setup
# ClawHub ships text, not file modes: the exec bit does NOT survive install. Without this, a
# cron `kind: "command"` dies with exit 126 and NO status line — the silence this skill prevents.
chmod +x scripts/preflight-queue.sh scripts/event-dispatch.sh \
scripts/oauth-expiry-check.sh scripts/purge-retention.sh # cron-message.sh is sourced: 644
# The scripts read nothing from the network until this file exists.
cat > scripts/secrets.env <<'EOF'
QUEUE_URL=https://api.acme-corp.example/queue
API_TOKEN=replace-me
EOF
chmod 600 scripts/secrets.env # never commit it; it is not shipped with this skill
| Variable | Where it lives | Who substitutes it |
|---|---|---|
QUEUE_URL, API_TOKEN | scripts/secrets.env | the shell, at script run time |
OPENCLAW_HOME, OPENCLAW_AGENT_ID | environment (optional) | the shell; default ~/.openclaw and main |
GATEWAY_TOKEN, MODEL_PRIMARY, MODEL_FALLBACK, RUNTIME_ID, PROVIDER, *_PLUGIN, CHANNEL, NOTIFY_CHANNEL | openclaw.json / job definitions below | you, by hand — OpenClaw does not interpolate ${...} in its config. A literal ${GATEWAY_TOKEN} left in the file is a literal token, and strict validation will not save you from it. |
(third-party, beyond metadata.requires) | an authenticated queue endpoint you own or may call, a model provider account, a notification channel | you, before the first run |
When to use
| Trigger | Action |
|---|---|
| "the agent stopped doing anything" | §OAuth first (silent auth death), then openclaw cron list for stuck jobs |
| "gateway won't start after I edited the config" | §Config as contract — strict validation rejects unknown keys |
| "how do I make my SaaS push to the agent?" | §The loopback cascade — you cannot. Invert to outbound poll |
| "my crons burn tokens finding nothing to do" | §Preflight layer — count without an LLM |
| "the cron fires manually but never on its event" | §Gotcha: real UUID job ids vs template ids |
| "setting up a dedicated always-on machine" | §Host constraints, then §Ops checklist |
Host constraints — the daemon is a user-level service tied to a GUI login session
| Constraint | Consequence you must implement |
|---|---|
| Service needs a logged-in session | A dedicated machine, auto-login, session never locked out. No headless server. Because the session is always unlocked, that machine must be physically secured, full-disk-encrypted, and carry no data unrelated to the agent — auto-login is only acceptable on a host with nothing else to lose. |
| Sleep kills the session | Disable disk/display sleep on AC power. |
| Restart is not automatic | A daily gateway restart cron (cr-20, 06:00) re-arms the loop. |
| A dead job stays dead | A watchdog cron 5 min later (cr-21, 06:05) lists jobs and reruns errored/stuck ones. |
| File-lock races | Keep ~/.openclaw out of any syncing folder (iCloud/Dropbox). |
| Stray env overrides | A launchctl setenv gateway token silently overrides config → permanent "unauthorized". Unset it. |
| Node version drift | nvm install 22 && nvm alias default 22 — check the CURRENT minimum (has been ≥22.x); a version manager keeps it identical across redeploys, the OS package does not. |
sharp fails to install | It compiles against a system libvips. SHARP_IGNORE_GLOBAL_LIBVIPS=1 npm install -g openclaw@latest, then openclaw --version → 2026.6.x (verify the live release; never hardcode it in a runbook). |
| macOS ships bash 3.2 | mapfile/readarray are bash 4+. The shipped scripts avoid them; keep anything you add 3.2-compatible. |
The loopback cascade — bind: "loopback" (§Config) answers only on 127.0.0.1
An anti-SSRF default, correct, and it decides your whole architecture:
| Consequence | What it forces |
|---|---|
| The gateway is unreachable from the public internet | No cloud service can ever push you a webhook — not your SaaS, not a git host, not a form backend. Their POST cannot arrive. |
| Every integration inverts | The agent polls outbound. The host is a client, never a server. |
| Freshness is not free | Push is instant; poll costs one request per interval — and an LLM turn per interval if you are careless. That price is what §Preflight exists to avoid. |
| The source of truth lives on the other side | The agent asks "is there work?", takes one item, acts, acknowledges. No ack = the item stays queued, so a killed run re-appears as work instead of vanishing. Crash-safety is free from that shape alone. |
OpenClaw does expose an inbound hook (POST 127.0.0.1:18789/hooks/agent, Bearer), reachable only from the host: useful for a local process to nudge the agent, useless from the internet. Exposing the control plane beyond loopback to "fix" this trades an SSRF-hardened default for a remote-code surface — don't. For local push, run a listener bound to 127.0.0.1 that authenticates a Bearer and accepts one event name from a closed list hardcoded in the listener, passing it as the dispatcher's only argument: no request body, no free-form arguments, no raw agent turn. scripts/event-dispatch.sh enforces that same closed list — an unrecognised event exits 2 without touching a job.
Config as contract
~/.openclaw/openclaw.json (JSON5) is strictly validated: one unknown key, one wrong type, and the gateway refuses to start. OpenClaw keeps a last-known-good copy and restores it when the file is broken — so a bad edit can leave you running yesterday's config while you read today's file. Confirm with a restart, never by reading.
{
gateway: {
port: 18789, mode: "local", bind: "loopback", // "local" mandatory; anything else = damaged
auth: { mode: "token", token: "${GATEWAY_TOKEN}" },
nodes: { denyCommands: ["camera.snap", "screen.record", "sms.send", // capture/publish/agenda
"contacts.add", "calendar.add", "reminders.add"] },
},
agents: {
defaults: {
model: { primary: "${MODEL_PRIMARY}", fallbacks: ["${MODEL_FALLBACK}"] },
models: { // MANDATORY allowlist: primary+fallbacks must appear here
"${MODEL_PRIMARY}": { agentRuntime: { id: "${RUNTIME_ID}" } },
"${MODEL_FALLBACK}": { agentRuntime: { id: "${RUNTIME_ID}" } } },
workspace: "${OPENCLAW_HOME}/workspace" },
list: [{ id: "main" }] },
plugins: { entries: { // EXCLUSIVE allowlist — omitted plugin = not loaded, silently
"${NOTIFY_PLUGIN}": { enabled: true }, // example — your chat/notification transport
"${PROVIDER_PLUGIN}": { enabled: true }, // example — your model provider
"${RUNTIME_PLUGIN}": { enabled: true }, // the runtime named by agentRuntime.id above; without
} }, // it, agentRuntime never executes
tools: { profile: "restricted" }, // narrowest profile that boots — jobs inherit THIS
commands: { native: "auto", restart: true },
}
| Rule | Failure mode if ignored |
|---|---|
| Strict validation | Unknown key → gateway won't boot. openclaw doctor --fix strips unknown keys (it lists them) and writes openclaw.json.bak. Read the diff — it deletes, it does not ask. |
plugins is an exclusive allowlist | A plugin you forgot simply doesn't load. No error. The feature is just absent. |
agents.defaults.models is an allowlist | Model in primary/fallbacks but not listed → Model not allowed, thrown before any response. |
denyCommands | Without it, a default set may allow capture/publish/agenda actions your always-on agent can fire unattended. |
commands.allowFrom | If set, it becomes the only authorization source and cancels pairing/allowlists. Don't set it without a reason. |
| Restart rule | Changes to gateway.*, plugins.*, browser.* need openclaw gateway restart. Others may hot-reload. When in doubt: restart. |
tools.profile is inherited | A job with no explicit payload.tools runs with the global profile. That is why the global must be the narrowest one that boots, never the widest: per-job grain is then a deliberate widening, not a hoped-for restriction. |
Auth and runtime are two orthogonal axes
Auth = which account pays (a provider profile: OAuth or an API key). Runtime = which executor runs the agentic turn (agentRuntime.id). Configuring one does not configure the other. All four must be present together — miss one and it breaks before the first response:
| # | Ingredient | Miss it → |
|---|---|---|
| 1 | model.primary in the live provider namespace | wrong/dead provider |
| 2 | agentRuntime.id set per model in agents.defaults.models | no executor |
| 3 | The runtime plugin enabled in plugins.entries | runtime never loads (allowlist) |
| 4 | auth.order. lists the profile | a profile outside order is never tried |
openclaw models list --provider ${PROVIDER} # never copy model names from a runbook — enumerate
# Output: no results <- legacy -/* namespace is dead; `doctor --fix` rewrites it
For an unattended host, use an API key. It is the supported path: pay-per-token, no expiry, no human in the loop. OAuth is subscription-priced but expires in days and needs a browser — on a 24/7 host you trade money against a recurring silent outage, and the outage wins. auth-profiles.json (in ~/.openclaw/agents//agent/) is a credential, not a config file — treat it like a private key: never in git, no SecretRef support. Copying it to another host is gray-area and not the recommended path: only acceptable if your provider's terms allow that account on that machine. Check the terms before you copy.
Failure #1: OAuth dies silently
A subscription OAuth token expires in days and does not refresh headless. The daemon keeps running; every turn just fails. Nobody is watching at 03:00. This is the single most common way an always-on agent dies.
OPENCLAW_HOME=~/.openclaw ./scripts/oauth-expiry-check.sh
# EARLIEST epoch expiry wins: a fresh profile must never mask a dead one, nor a 90-day refresh token
# a dead access token. It names the deciding `field` and NEVER exits non-zero. "Cannot determine" is
# NEVER `ok`: corrupt JSON, no python3, an unreadable (ISO-8601) expiry all emit `warn` — the mtime
# proxy is reached ONLY when the file parses and genuinely carries no expiry field at all.
# Output: {"status":"warn","source":"expires_at","field":"profiles.dead.expires_at","remaining_days":-1,"note":"...relogin..."}
# Output: {"status":"warn","source":"unresolved","reason":"corrupt_json","note":"Cannot verify token expiry..."}
Wire it to cr-23 (below), whose only instruction is: run the script, and if status is warn or missing, alert ${NOTIFY_CHANNEL} that a human relogin is needed. The cron must not attempt the login itself — that flow needs a real browser. Alert on warn; stay silent on ok.
Cron layer
The scheduler is in-process in the gateway; jobs persist to ~/.openclaw/cron/jobs.json. No system crontab.
{ id: "cr-10-worker", agentId: "main", enabled: true,
schedule: { kind: "cron", expr: "0 9-18 * * 1-5", tz: "UTC" }, // IANA tz; or kind:"every"/"at"
sessionTarget: "isolated", wakeMode: "now", // isolated = inherits no context, no credentials-in-
payload: { kind: "agentTurn", message: "…", // conversation, no half-done state from another run
model: "${MODEL_PRIMARY}", fallbacks: ["${MODEL_FALLBACK}"],
tools: ["exec", "read"], // ALWAYS explicit: a read-only audit and a job that
thinking: "medium", // pushes branches must not share an authority
timeoutSeconds: 2400 }, // surface just because they share a host
delivery: { mode: "announce", channel: "${CHANNEL}", to: "${NOTIFY_CHANNEL}" },
failureAlert: { after: 2, to: "${NOTIFY_CHANNEL}", cooldownMs: 1800000 } }
The four housekeeping jobs the §Ops checklist watches — argv[0] is an absolute path in every case, a cron's PATH is not your shell's — then the four laws for anything that runs unattended:
| id | Schedule | Payload |
|---|---|---|
cr-20-restart | { kind: "cron", expr: "0 6 * * *", tz: "UTC" } | { kind: "command", argv: ["/opt/homebrew/bin/openclaw", "gateway", "restart"] } |
cr-21-watchdog | { kind: "cron", expr: "5 6 * * *", tz: "UTC" } | { kind: "agentTurn", tools: ["exec"], message: "Run openclaw cron list. Rerun errored/stuck jobs. Report one status line." } |
cr-22-purge | { kind: "cron", expr: "0 5 * * 0", tz: "UTC" } | { kind: "command", argv: ["/abs/path/to/scripts/purge-retention.sh"] } |
cr-23-oauth | { kind: "cron", expr: "0 8 * * *", tz: "UTC" } | { kind: "agentTurn", tools: ["exec"], message: "Run scripts/oauth-expiry-check.sh. If status is warn or missing, alert that a human relogin is needed. Silent on ok." } |
- Idempotence first. A cron wakes with no memory. Before acting, read the state: your own index file and the remote's. In doubt, SKIP — a skipped run costs nothing, a duplicate action costs trust.
- Failure = no ack. If a check goes red, don't acknowledge. The item stays queued and a healthy later run takes it. Never ack to make an alert stop.
- Bounded backoff, not correction. The scheduler retries a failed run with bounded backoff (roughly ~30s → 1m → 5m → 15m, ~3 attempts, reset on success). Never depend on retry to fix a logic failure — that's what the un-acked queue is for.
- One normalized status line. Every job and script emits the same shape via
scripts/cron-message.sh. Without it, ten jobs invent ten formats and the channel becomes unreadable within a week.
Output format — every job, every script
-
Result:
Item:
Next action:
Details:
dispatch - Started <- emitted by the DISPATCHER when a preflight counts > 0;
Result: Job requested. the preflight stays silent and execs it. Verified output.
Item: cr-10-worker
Next action: OpenClaw runs it now.
Details: event=work_ready count=2
Preflight layer
Stop paying an LLM turn to discover there is nothing to do. Split every heavy job in two: the preflight counts and, if count > 0, calls the dispatcher with a named event; the dispatcher runs the matching job(s) now. The heavy job keeps a widely-spaced every schedule as a safety net for the day a preflight silently breaks. Result: near-event-driven latency at poll-loop cost, with a floor under it.
| Layer | Kind | Frequency | Cost |
|---|---|---|---|
pf-* preflight | kind: "command" (argv → shell script) | every 5–15 min | one curl + a count. Zero LLM. |
cr-* heavy job | kind: "agentTurn" | every 6h fallback only | a full agent turn |
{ id: "pf-10-queue", enabled: true, schedule: { kind: "every", ms: 600000 }, // 10 min
payload: { kind: "command", argv: ["/abs/path/to/scripts/preflight-queue.sh"] } } // cron PATH != yours
THE PREFLIGHT DECIDES NOTHING. IT COUNTS. It must never claim an item, mutate state, or reason about what to do — the moment a preflight starts judging, you have a second, cheaper, untested agent making production decisions in bash. Count, dispatch, exit.
every does not spread a fleet — it phase-locks it. The table above tells you to give each heavy job a widely-spaced every as its safety net. Do that for five jobs in one provisioning pass and all five fire together, forever. Interval schedules do not drift: equal intervals preserve whatever phase the jobs were created with, and a fleet provisioned in one pass was created seconds apart. Pick a round anchor and it is worse — five jobs at everyMs: 21600000 created on a timestamp that divides evenly by 6h land on the same 00/06/12/18 grid to the second. Five agent turns spawning worktrees, installs and a browser at the same instant on one host is a safety net that has become the outage.
Two things sharpen it. The fallback bypasses your only concurrency guard: the scheduler fires the job directly, so it never passes through event-dispatch.sh and neither the mkdir lock nor the runningAtMs check is in the loop — the one moment the fleet is most likely to collide is the one moment nothing is guarding it. And the daily restart re-anchors: cr-20 reloads every job at once, so a fleet you hand-staggered can silently re-converge after a reboot.
| Do | Not |
|---|---|
Give each heavy fallback an explicit cron expression on its own minute — the way cr-20…cr-23 above already are (0 6, 5 6, 0 5, 0 8). openclaw cron list then shows you the phase plan. | Five every: 6h jobs and the assumption they will drift apart. They will not. |
staggerMs if your release carries it — confirm with openclaw cron get on the version you actually run, never from a runbook. | A phase plan that exists only as arithmetic nobody re-derives at 03:00. |
Verify rather than trust the shape: openclaw cron list, read the next-run times, and check that no two heavy jobs share a minute. If they do, you have not built a safety net — you have built a thundering herd with a 6-hour period.
./scripts/preflight-queue.sh
# Tolerant count: numeric `count`, else the SHALLOWEST array anywhere (nested {data:{items:[…]}} still
# counts; an API renaming `items` to `results` degrades to a count, not a crash loop). "Counted zero"
# is NOT "found nothing to count": an empty array is 0, but no-count-and-no-array — a 200 carrying
# {"error":"unauthorized"}, 502 HTML, a captive portal — is an Error. A green "Nothing to do" while the
# queue fills is worse than a crash. No secrets.env, dead host, junk body -> one status line, exit 0,
# never a bare shell error. Hence no `set -e`: a broken preflight must still produce a line.
# Output: pf-10-queue - Nothing to do / Result: Queue empty. / ... / Details: count=0
# Output: pf-10-queue - Error / Result: Payload carries no count and no array... / Details: reason=no_countable_field
scripts/event-dispatch.sh maps events → jobs, with three guards:
mkdirlock per job id — atomic, no race, released onRETURN,EXIT,INTandTERM, plus a TTL.trap ... RETURNalone is not enough: akill -9, the 06:00 restart or a sleeping machine leaves the dir behind and blocks that event forever, while the channel keeps printing a healthy-lookingBlocked. A lock older thanLOCK_TTL_MIN(default 15) is reclaimed.- Job exists —
openclaw cron getfirst. A non-zero exit, an empty answer, or a reply that is not JSON carrying that id is anErrorline naming the fix. It classifies into exactlyunknown | running | idle, so a release whosecron getexits 0 and prints prose ("job not found") cannot fall through to a run on a bogus id. Verify yours:openclaw cron get nope; echo $?. runningAtMsguard — if the job is mid-run, reportBlockedand return 0. Never stack turns on a running job.
One event may fan out to several jobs (source_changed → reviewer + drift-watch). The shipped mapping's cr-30-reviewer and cr-40-drift-watch are illustrative placeholders — this skill defines only cr-10-worker and cr-20…cr-23; until you point them at jobs you installed, those two events dispatch to nothing but an Error line (which is the designed failure, not a silent one). Keep the event vocabulary about what happened in the world (work_ready, review_needed, source_changed), never about which job you want to run — that's the mapping's job, and it's the only place that changes when you rename a cron.
Gotchas
| Symptom | Root cause | Fix |
|---|---|---|
| Event dispatch reports success, job never runs | openclaw cron add accepts no --id: the installed job gets an auto-generated UUID, and its name is prefixed. Your template's tidy cr-30-reviewer exists only in your file. The dispatcher's openclaw cron run cr-30-reviewer targets a job that was never installed — and it fails in a way that reads like nothing happened. Every job created via CLI is affected; only hand-seeded jobs.json entries keep their ids. | openclaw cron list, copy the real ids, and point the mapping at those. Re-check after any re-provisioning: new install, new UUIDs. |
| Script dies with no status line, exit 126 | The exec bit did not survive install — ClawHub ships text, not modes | chmod +x scripts/*.sh (§Setup). cron-message.sh stays 644 |
| Feature configured but absent, no error | plugins allowlist is exclusive — the entry is missing | Add it to plugins.entries and restart |
Model not allowed before any answer | Model used but not in agents.defaults.models | Add it with its agentRuntime |
| Gateway won't boot after an edit | Strict validation rejected an unknown key | openclaw doctor --fix, read the stripped-key list and the .bak |
| Config edited, behaviour unchanged | Last-known-good restored, or gateway.*/plugins.* needs a restart | openclaw gateway restart, then verify live state |
| Persistent "unauthorized" on loopback | A launchctl setenv token overrides the config file | Unset it, restart |
Dispatch always says Blocked, no job ever runs | A killed run (restart, sleep, kill -9) orphaned its lock dir; the channel looks healthy | rm -rf "${TMPDIR:-/tmp}"/openclaw-event-*.lock. The TTL reclaims it after LOCK_TTL_MIN; lower it if your jobs are short |
cr-23 says ok while every turn fails | Several profiles in one auth file, and a fresh one masked the dead one — the shipped checker takes the earliest expiry precisely for this | Read field in the JSON: it names the profile that decided. If you wrote your own checker, take the min — and make "cannot determine" a warn, never an ok |
cr-22 reports Not an OpenClaw home | OPENCLAW_HOME points somewhere without openclaw.json or agents/ — a typo or a stale env var | Intended: the purge refuses any dir it cannot prove is an agent home rather than deleting on faith. Fix the variable |
| All runs fail overnight, no alert | OAuth expired headless | cr-23 + human relogin; consider an API key |
| Notification channel unreadable | Jobs invented their own formats | One cron-message.sh, no exceptions |
Ops checklist
| Watch | Cadence | Symptom → cause |
|---|---|---|
openclaw gateway status | daily (cr-20 restart 06:00) | not up → session logged out / machine slept |
openclaw cron list | daily (cr-21 watchdog 06:05) | errored/stuck jobs → rerun; missing next-run → scheduler restart |
oauth-expiry-check.sh | daily (cr-23 08:00) | warn/missing → human relogin, today |
Preflight + dispatch status lines | continuous | reason=no_countable_field → the endpoint changed shape or the token died (it self-reports; it does not print a green Nothing to do); dispatch - Error → mapping points at dead ids |
| Heavy-job fallback runs | weekly | fallback firing every time → the preflight is broken, fix the preflight |
| Logs + workspace size | weekly (cr-22 purge 05:00 Sun) | logs past 30d → purged automatically; workspace growing → set your own bound |
openclaw --version + Node | on update | after openclaw update: doctor, gateway restart, health |
Scope
This skill ONLY: configures an OpenClaw host you control; polls endpoints you own or are authorized to call; schedules jobs with per-job tool restrictions; monitors auth expiry and alerts a human; deletes only its own logs past the stated window; emits normalized status lines.
This skill NEVER: exposes the control plane beyond loopback; bypasses an anti-abuse control, rate limit, or authentication check — when a defence triggers, the run stops and pings a human instead of adapting; performs an irreversible action unattended; conceals a failure behind an ack; writes secrets into the workspace, the notification channel, or git.
Alexandre Bloch — Bloch Agents · ClawHub @AlexBloch-IA
相关技能
Use when an OpenClaw gateway-backed agent is asked through Discord, Slack, Matrix, SMS, web chat, CLI relay, ACP, cron, or another channel to restart, stop,...
Build, bootstrap, and maintain a stable OpenClaw default agent identity by interviewing the user, updating IDENTITY.md, and seeding core context files (AGENT...
Run a hosted agent on a cron schedule — daily digests, uptime monitors, recurring scrapes, periodic reports — that fire on their own and bill exactly-once wi...
Diagnose, audit, and repair OpenClaw hosts when an agent, gateway, plugin, cron, model route, memory engine, channel, approval policy, or update looks broken...
Monitor OpenClaw Agent health, uptime, task status, and system metrics. Automatic alerts on failures, performance degradation, or unusual behavior. Includes...