WATCH & Triggers
WATCH observes a resource continuously and fires an expression automatically when a real change occurs — no incoming request required. It shares the exact same persistent, registry-backed architecture as SCHEDULE & Cron Jobs, applied to an observed condition instead of a fixed moment or interval.
One Form, Any Resource
NEW WATCH [check] EVERY n UNIT ONCHANGE [trigger] is the whole grammar. The check
is an ordinary inline expression; what is being watched is simply whatever it returns. A database,
a file, a URL, a stored value and anything else expressible inline all use the same shape —
there is no per-resource syntax to learn.
[FETCH "url"] emits nothing and still works: the fetched result is its return value,
and that is what the baseline holds. Adding EMIT is unnecessary. If a check does emit,
its printed output is what gets compared instead.Writing the Triggered Expression
A watch fires later, in a script of its own — the script that registered it is long gone,
and operations are script-scoped. Both brackets take one statement, so an
AFTER chain inside a bracket will not parse. Use
SIDELOAD to carry a whole body inline, or
INCLUDE to point at a reusable file.
NEW WATCH [COUNT ROWS FROM DB "shopdb" TABLE "orders"] EVERY 1 MINUTE ONCHANGE [SIDELOAD `
NEW OPERATION notify_new_order
OPEN
COUNT ROWS FROM DB "shopdb" TABLE "orders" SET ?n
AFTER MAIL TEXT BODY "Order count is now " & ?n TO "admin@example.com" SUBJECT "New Order"
CLOSE
AFTER RUN notify_new_order
`] SET ?watch
NEW WATCH [COUNT ROWS FROM DB "shopdb" TABLE "orders"] EVERY 1 MINUTE ONCHANGE [INCLUDE "/root/ops/notify_new_order.oql"] SET ?watch
SIDELOAD keeps everything in the
registered row, and a parse error inside it is an ordinary catchable error. INCLUDE
shares one file across many jobs, but a missing include is INFINITE —
uncatchable. Delete the file and every fire records status = failed.Watching a Database Table
[SELECT ROWS FROM DB "shopdb" TABLE
"orders" WHERE "status" IS EQUAL TO "pending"] fires whenever that result differs from the
one captured when the watch was registered.Watching a File
NEW WATCH [FILE EXISTS "/root/uploads/invoice.pdf"] EVERY 1 MINUTE ONCHANGE [INCLUDE "/root/ops/process_upload.oql"] SET ?watch
NEW WATCH [FILE READ "/root/config.txt"] EVERY 1 MINUTE ONCHANGE [INCLUDE "/root/ops/on_config_file.oql"] SET ?watch
[FILE EXISTS "path"] goes false → true when a file
appears; [FILE READ "path"] fires whenever the contents differ. Detection is by
polling at your chosen interval — not instant.Watching a PERSISTENT Value
NEW WATCH [RECALL "config"] EVERY 1 MINUTE ONCHANGE [INCLUDE "/root/ops/on_config_changed.oql"] SET ?watch
Watching an External URL
The same form again, pointed outward: WATCH is not limited to internal OcaltQL state — it can poll any external resource and fire when the fetched result differs from what was captured at registration time.
NEW WATCH [FETCH "https://ql.ocalt.com/tutorial/fixture.html"] EVERY 1 HOUR ONCHANGE [SIDELOAD `
MAIL TEXT BODY "The watched page changed" TO "admin@example.com" SUBJECT "Page Changed"
`] SET ?watch
Intervals
Ten unit words are accepted: SECOND SECONDS MINUTE
MINUTES HOUR HOURS DAY DAYS
WEEK WEEKS. The master scheduler wakes every 10 seconds,
so that is the effective floor — EVERY 1 SECOND behaves as roughly ten. An
interval is a minimum, not a grid: the next poll is scheduled from the moment the last one
finished, so a watch drifts progressively later rather than holding the clock.
Lifecycle, Status and Errors
A watch is a standing observer. It fires on a change, stores the new value as its baseline, and
re-arms — so the next change fires it again, indefinitely. It is never marked
done and never removed automatically; deleting it is your call.
WATCH LIST and WATCH DESCRIBE return done,
status (pending, ok or failed) and, on
DESCRIBE, last_error with the failure message verbatim.
EVERY 1 MINUTE costs at least 1,440 queries a day before it ever fires.Registry — List, Delete, Describe
WATCH LIST SET ?watches
AFTER FOREACH ?watches SET ?key AS ?watch
OPEN
WATCH DESCRIBE ?watch SET ?info
AFTER EMIT ?info & "\n"
CLOSE
WATCH LIST SET ?watches
AFTER FOREACH ?watches SET ?key AS ?watch
OPEN
WATCH DESCRIBE ?watch SET ?info
AFTER WATCH DELETE ?watch
CLOSE
ocalt-core. A master
scheduler wakes every 10 seconds, takes whatever is due, and runs each one as its owner through the
ordinary ql-runtime API — same endpoint, same credentials, same quota and
namespace as a request you make yourself. Every kind is polling-based without exception, so
detection is bounded by your interval. Runs execute in parallel and each is allowed the full
300-second OcaltQL ceiling, so a slow watch never holds up anyone else's. Windows missed while the
scheduler is down are not replayed — the job runs once when it returns and re-schedules from
that moment.