Wake up to what changed.
Point an agent at any URL, file, or command. watchd diffs it against last run, keeps state in SQLite, and notifies you when something changes.
Watch. Diff. Decide.
Notify.
watch.url() fetches and auto-diffs against the previous run. ctx.judge() sends the diff to an LLM for a yes/no verdict. ctx.notify() pushes to Slack, a webhook, or stdout. No plumbing code.
from watchd import agent, watch
@agent(every="12h")
def pricing_watch(ctx):
change = watch.url("https://acme.com/pricing", ctx=ctx)
if change is None:
return "no change"
verdict = ctx.judge(change, instruction="Did the price increase?")
if verdict.should_act:
ctx.notify(f"Price alert: {verdict.summary}", channel="slack")
return verdict.summary$ watchd run pricing_watch ✓ pricing_watch (a3dc91b20f4e) in 812ms result: no change $ watchd run pricing_watch ✓ pricing_watch (7f2cb8e5d013) in 943ms result: Price increased from $49 to $79 on Starter plan $ watchd status Agent Schedule Last Run Duration When pricing_watch 12h 7f2cb8e5d013 943ms 2m ago
$ watchd state pricing_watch
{
"_watch:url:https://acme.com/pricing": "<html>...",
}
$ watchd history pricing_watch --as-json
[
{
"id": "7f2cb8e5d013",
"status": "success",
"result": "Price increased from $49 to $79",
"duration_ms": 943,
"started_at": "2026-03-09T08:00:01"
}
]Cron can schedule. It cannot diff.
watchd gives you watch primitives, persistent state, and notifications as defaults. Your agent detects what changed without any plumbing code.
Point it at something.
A URL, a file, a command output. watchd fetches it on schedule and auto-diffs against the previous run. Zero plumbing.
# zero-code mode $ watchd watch https://acme.com/pricing --every 5m # or drop a file in watchd_agents/ $ watchd init $ watchd new pricing_watch
Let an LLM judge the diff.
watch.url() returns a Change with before, after, diff, and summary. Pass it to ctx.judge() for an AI verdict, or write your own logic.
change = watch.url("https://acme.com/pricing", ctx=ctx)
if change is None:
return "no change"
verdict = ctx.judge(change, instruction="Did the price go up?")
if verdict.should_act:
ctx.notify(verdict.summary, channel="slack")Deploy once, inspect anytime.
Push to any VPS with one command. Rich CLI with status dashboard, JSON output, and full run history.
$ watchd run pricing_watch ✓ pricing_watch (7f2cb8e5d013) in 943ms $ watchd status ✓ pricing_watch 12h 7f2cb8e5d013 943ms 2m ago $ watchd deploy
Agents you can run tonight.
Copy any of these into watchd_agents/. Each one watches something real, diffs against the previous run, and notifies you when something changes.
Watch your certs. Get alerted 14 days before they expire.
@agent(every="12h")
def ssl_watch(ctx):
change = watch.command(
"echo | openssl s_client -connect mysite.com:443 2>/dev/null"
" | openssl x509 -noout -enddate",
ctx=ctx,
)
if change:
ctx.notify(f"SSL cert changed: {change.summary}", channel="slack")Watch df, free, or uptime output. Diff catches spikes cron misses.
@agent(every="5m")
def infra_watch(ctx):
change = watch.command("df -h / | tail -1", ctx=ctx)
if change:
verdict = ctx.judge(change, instruction="Is disk usage above 85%?")
if verdict.should_act:
ctx.notify(verdict.summary, channel="slack")Tail mode returns only new lines since last check. No state plumbing.
@agent(every="30s")
def error_watch(ctx):
change = watch.file("/var/log/app.log", ctx=ctx, mode="tail")
if change and "ERROR" in change.new:
ctx.notify(f"{change.summary}: {change.new[:200]}", channel="slack")Watch a JSON endpoint. Auto-diff catches schema changes, new fields, removed keys.
@agent(every="1h")
def api_watch(ctx):
change = watch.url("https://api.vendor.com/v2/status", ctx=ctx)
if change:
verdict = ctx.judge(change, instruction="Did the API schema change?")
if verdict.should_act:
ctx.notify(f"API drift: {verdict.summary}", channel="webhook",
url="https://myteam.com/hooks/alerts")Watch a vendor page. Let an LLM judge if the change affects your contract.
@agent(every="1d")
def tos_watch(ctx):
change = watch.url("https://vendor.com/terms", ctx=ctx)
if change:
verdict = ctx.judge(
change,
instruction="Did anything change about pricing, liability, or data retention?"
)
if verdict.should_act:
ctx.notify(f"ToS alert: {verdict.summary}", channel="slack")Scrape a pricing page. Diff detects plan changes, new tiers, price bumps.
@agent(every="6h")
def pricing_watch(ctx):
change = watch.url("https://competitor.com/pricing", ctx=ctx)
if change:
verdict = ctx.judge(change, instruction="Did any price change?")
if verdict.should_act:
ctx.notify(verdict.summary, channel="slack")
return verdict.summaryYou write agent logic. watchd handles the rest.
Watch primitives, state, notifications, AI judge, history, deploy. One SQLite file, zero external services.