Install
openclaw skills install @anatoli-iliev/web-observerWatches websites and reports what broke: uptime checks that alert when a site goes down and again when it recovers, Vercel error logs and Core Web Vitals, and GA4 traffic. Ask "is my site up", "did anything error on Vercel", or "how much traffic last week".
openclaw skills install @anatoli-iliev/web-observerThree questions about a website, from one command line. Is it up? Uptime checks on a per-URL interval, with one alert when a site goes down and one when it comes back. Is it erroring or slow? Vercel request logs and Core Web Vitals. Is anybody visiting? GA4 traffic.
Only the first is implemented here. Vercel and GA4 are answered by two other skills, and Web Observer never reimplements either.
Run it as:
node <skill-dir>/lib/cli.js <command>
1. Which question is it? "Is the site up" is check. "What broke" is
vercel errors. "How many visitors" is ga4 report. The table below routes the
usual phrasings.
2. Is it configured? No credential is needed for uptime watching. If a command reports a missing configuration, say which file it named and offer to add a watch, rather than guessing a URL.
3. Run it and read the answer back. The table output is already laid out for a person: quote it rather than re-typesetting it, then add the one sentence of interpretation the numbers support.
| The user says | Run |
|---|---|
| "is my site up", "check the site now", "are any sites down" | check |
| "check just the blog" | check --only blog |
| "did example.com go down last night" | doctor --json, then read the state file it names; this tool alerts on change and does not keep a history |
| "set up monitoring", "why is this not working", "it never alerts me" | doctor --json |
| "start watching", "schedule it", "make it run every 5 minutes" | schedule (prints the commands), then schedule --apply |
| "what errors did my site have", "why am I getting 500s", "show me the logs" | vercel errors --since 30m |
| "which routes are failing", "group the errors" | vercel error-summary --since 6h |
| "how fast is the site", "what are my Core Web Vitals" | vercel vitals |
| "which pages are slowest" | vercel slowest-pages |
| "how is my traffic", "top pages this week", "where do visitors come from" | ga4 report, ga4 query, or vercel top-pages |
| "who is on the site right now" | ga4 live |
| "test the alerts without spamming me" | watch --dry-run |
watch, vercel-watch and digest are for the scheduler, not for
conversation. Running them by hand consumes the one alert an outage gets. Use
check to look at things now, or add --dry-run.
check and match
their words against the watch ids it prints. If several could match, ask. Do
not invent a URL and do not add one to the configuration without being asked.vercel overview will list the
account's projects if none is configured. List them and ask; do not pick one.doctor --json. The usual answer is that
the cron job was never created, which blocked_on: "no_cron_jobs" names.--jsonPrefer the table when relaying an answer. Add --json only when a figure has to
be computed: comparing two runs, or pulling one number into a sentence.
doctor --json is the exception that is always right, because its shape is a
next-step state machine rather than a report.
| Code | Meaning | What to say |
|---|---|---|
| 0 | Worked | Report the answer. A run that finds everything healthy is a success, and so is an error query that returns nothing. |
| 1 | A check or a delegated call failed for a network or API reason | Say the check failed, and that this is not the same as the site being down. |
| 2 | Configuration or usage error | Quote the message: it names the setting and the fix. Nothing was attempted. |
| 3 | Worked, and something is down or over budget | Only from check --strict. Report what is down. |
| 130 | Interrupted | Say it was interrupted. |
vercel errors themselves, or
set includeMessages: true knowing what it means.openclaw config set.| Command | What it does |
|---|---|
check | Runs every configured watch once, now, and prints a table. Does not change alert state. |
watch | The scheduled uptime round. Checks what is due, prints an alert or NO_REPLY. |
vercel-watch | The scheduled Vercel error-log and performance-budget poll. |
digest | The scheduled GA4 traffic digest. |
vercel <preset> [flags] | Ask the vercel-insights skill. |
ga4 <preset> [flags] | Ask the open-ga4 skill. |
doctor | Configuration, dependency and schedule diagnosis. |
schedule | Prints the openclaw cron add commands this configuration needs. |
| Flag | On | What it does |
|---|---|---|
--json | check, doctor, schedule | Machine-readable output. |
--strict | check | Exit 3 when anything is down. For CI. |
--only ID | check | Limit to named watches. Repeatable or comma separated. |
--dry-run | check, watch, vercel-watch, digest | Make no lasting change and deliver nothing. |
--apply | schedule | Actually create the cron jobs. |
An unrecognised flag is a usage error, never ignored. A dry run that quietly was not one would be worse than no dry run at all.
An OpenClaw cron job with a command payload and --announce delivers the
command's stdout to a chat channel. Printing the exact token NO_REPLY
suppresses that delivery. So:
NO_REPLY.That is the whole notification mechanism. Web Observer opens no connection to Telegram or anything else, and holds no bot token. Alerts travel over whatever channel the cron job names, which is the channel already configured in OpenClaw.
Two consequences worth knowing:
watch exits 0 even when it alerts. An alert is this tool working. A
non-zero exit would additionally fire OpenClaw's own cron failure
notification, so one outage would be reported twice by two mechanisms.openclaw skills install then a configuration file. Uptime needs no
credentials, so nothing else is required for it.
node <skill-dir>/lib/cli.js doctor
doctor names the configuration file it expects. Copy examples/config.json
from this skill's directory to that path and edit it. Then:
node <skill-dir>/lib/cli.js check
node <skill-dir>/lib/cli.js schedule
node <skill-dir>/lib/cli.js schedule --apply
SETUP.md in this skill's directory is the click-by-click version, including
the Vercel and GA4 modules and a troubleshooting table.
Each of uptime, vercel and ga4 has its own enabled, and each command
checks only the module it needs. Web Observer installed for uptime alone is
ready the moment it is installed: requires names node and nothing else, and
nothing in requires.env, so it is never held in "needs setup" for a credential
it does not use.
vercel needs the vercel-insights skill; ga4 needs open-ga4. When one is
absent, the command that needed it says so, names the install command, and exits
2. Nothing else stops working.
One JSON file, by default <openclaw-state>/web-observer/config.json. Not in
openclaw.json, because every install route replaces the skill directory and a
list of watches is awkward to edit with openclaw config set.
{
"notify": { "channel": "telegram", "to": "telegram:123456789" },
"uptime": {
"enabled": true,
"tickMinutes": 5,
"defaults": { "timeoutMs": 10000, "failureThreshold": 2 },
"watches": [
{ "id": "site", "url": "https://example.com", "intervalMinutes": 5 },
{
"id": "api",
"url": "https://example.com/health",
"intervalMinutes": 1,
"expectBody": "\"ok\":true",
"failureThreshold": 1
}
]
}
}
| Setting | Default | Meaning |
|---|---|---|
id | required | Keys this watch's saved state. Renaming it forgets whether the URL was down. |
url | required | http or https. Credentials in the URL are refused; use headers. |
intervalMinutes | 5 | How often to check. Each check lands on the nearest scheduled round, so setting this to tickMinutes or below means every round checks it. |
method | GET | GET, HEAD or OPTIONS only. A health check must not be able to change anything. |
expectStatus | [200] | A code (200), a class ("2xx"), a range ([200, 399]), or a list of those. |
expectBody | none | Substring the body must contain. Catches a 200 that renders an error page. |
expectBodyRegex | none | A pattern instead of a substring. Not both. |
timeoutMs | 10000 | Per request. |
attempts | 2 | Requests within one check. Succeeds as soon as one does. |
retryDelayMs | 10000 | Pause between attempts. |
failureThreshold | 2 | Consecutive failed checks before the first alert. |
recoveryThreshold | 1 | Consecutive successes before the recovery message. |
followRedirects | 0 | Hops to follow. Every hop is allowlist-checked. |
headers | none | Sent as given. Newlines are refused. |
enabled | true | A disabled watch is never checked. |
attempts and failureThreshold guard against different things at different
speeds. attempts retries seconds apart inside one run, which catches a dropped
packet. failureThreshold waits for the next tick, minutes later, which catches
a flaky minute. The defaults mean four failed requests over about five minutes
before anybody is woken up.
{
"vercel": {
"enabled": true,
"project": "my-project",
"errors": {
"intervalMinutes": 15,
"windowMinutes": 20,
"threshold": 0,
"includeMessages": false
},
"budget": { "intervalMinutes": 360, "metrics": { "lcp": 2500, "inp": 200, "cls": 0.1 } }
},
"ga4": {
"enabled": true,
"property": "123456789",
"digest": { "cron": "0 9 * * 1", "preset": "report", "since": "7d" }
}
}
windowMinutes must exceed intervalMinutes. The overlap is what stops an
error arriving mid-poll from falling into the gap between two windows, and it
costs nothing because errors are deduplicated by request id.
includeMessages defaults to false and should stay there unless you have
thought about it: see the rule above about raw log text.
Web Observer contacts exactly the URLs configured under uptime.watches, and
nothing else. That is enforced in code, not merely documented:
redirect-off-allowlist, naming the host, and is never followed. So does a
redirect from https to http, even on the same host.headers are sent only to the watch's own host. A redirect to
another watch's host is followed without them.fetch is never allowed to
follow one itself, so the allowlist binds every hop rather than only the first.GET, HEAD and OPTIONS are ever issued.Everything Vercel and GA4 is a subprocess: those skills make those requests with their own credentials and their own allowlists. Web Observer passes arguments as an argument vector, never through a shell.
Declared here because no manifest field can say it without side effects:
requires.config is an eligibility gate in OpenClaw, not a declaration, and
listing these keys there would hold an uptime-only install in "needs setup".
Uptime watching reads no credential and no other skill's files. Beyond its own configuration and state file it touches nothing.
The Vercel and GA4 modules read <openclaw-state>/openclaw.json, and from it
only these keys:
skills.entries.vercel-insights.apiKey and skills.entries.vercel-insights.env:
the Vercel token and the project and team ids from the variables declared
above.skills.entries.open-ga4.apiKey and skills.entries.open-ga4.env: the GA4
credentials and property id from the variables declared above.skills.entries.<slug>.enabled for those two slugs, to report a disabled
skill rather than run it.agents.defaults.workspace, to find where those two skills are installed.They are read only when you run a vercel or ga4 command, or when doctor,
vercel-watch or digest covers a module you enabled. Why they are read at
all: a scheduled cron job runs with the Gateway's environment rather than a
skill's, so the credentials the delegated skill is configured with are absent
from a scheduled run unless something reads them. Reading them keeps the secret
in one place instead of asking you to configure it twice.
A value read this way is passed only in the environment of the subprocess that
runs that same skill. It is never put on a command line, never printed, never
written to a file, and never sent anywhere by Web Observer itself. A value that
is a secret reference, or a ${VAR} interpolation of an unset variable, is
reported by name and not resolved.
Also read: each delegated skill's SKILL.md for its version, and whether its
.venv and vercel_insights/logs.py exist.
Written: only the state file beside the configuration (default
<openclaw-state>/web-observer/state.json, mode 0600). schedule --apply also
creates cron jobs, by running openclaw cron add. It runs only when you pass
--apply, and it never replaces a job that already exists.
timeout, dns, connection-refused, tls, status-mismatch,
body-mismatch, redirect-off-allowlist, network. Each alert names one, with
the observed value behind it.