WATCHD
WATCH. DIFF. ACT.
FIG. 01 / THE OUTCOME

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.

watchd_agents/pricing_watch.py8 LINES
WATCHURL / FILE / CMD
STATEPERSISTENT
HISTORYQUERYABLE
DEPLOY1 COMMAND
FIG. 02 / PROOFWATCH + JUDGE + NOTIFY

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.

AGENT LOGIC
watchd_agents/pricing_watch.py
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
CLI OUTPUT
watchd run + watchd status
$ 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
STATE + HISTORY
watchd state + watchd history
$ 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"
  }
]
FIG. 03 / WHY NOT RAW CRON

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.

CRON + CUSTOM SCRIPT
01Scrape at 03:00, print current value. Gone by next run.
02No prior snapshot unless you build your own storage layer.
03No diff. You see current state, never what changed.
04No history table unless you wire your own schema and DB.
05Alert fires, but you still reconstruct context by hand.
WATCHD
01watch.url() auto-diffs against the previous fetch.
02ctx.state persists between runs. SQLite, zero config.
03Change object has before, after, diff, and summary built in.
04history, logs, state queryable from one CLI or --as-json.
05ctx.notify() sends the diff context to Slack or any webhook.
FIG. 04 / HOW TO GET VALUE TONIGHT
01WATCH

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
02DECIDE

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")
03SHIP

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
FIG. 05 / REAL AGENTS6 EXAMPLES

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.

SSL cert expiry

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")
Disk / memory / load

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")
Log file tailing

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")
API response drift

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")
Terms of service changes

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")
Competitor pricing

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.summary
FIG. 06 / UNDER THE HOOD11 MODULES

You write agent logic. watchd handles the rest.

Watch primitives, state, notifications, AI judge, history, deploy. One SQLite file, zero external services.

scheduler
Cron + interval triggers
runner
Execute with retries and capture
watch
URL, file, command auto-diffing
state
Persistent memory across runs
notify
Slack, webhook, or stdout alerts
judge
LLM verdict on any change
logger
Structured, queryable logs
retry
Automatic backoff on failure
history
Every run tracked and searchable
deploy
Git pull + restart, any VPS
cli
status, run, watch, logs, deploy
cli -> schedulercli -> deployscheduler -> runnerrunner -> staterunner -> watchrunner -> loggerrunner -> retryrunner -> historywatch -> statewatch -> notifyjudge -> watchnotify -> logger
FIG. 07 / TWO WAYS TO START
Star on GitHub
BUILT BY © LEVEL09