Schedule & Cron Jobs

SCHEDULE runs an expression once, at a future moment. CRON runs one repeatedly, on a fixed interval. Both register a persistent job in Ocalt's own scheduler — the script that created the job does not need to stay running, or even exist, for the job to fire later.

Writing the Job Body

A registered job runs later, in a script of its own — the script that registered it is long gone, and operations are script-scoped. The bracket also takes one statement, so an AFTER chain inside it will not parse. Use SIDELOAD to carry a whole body inline, or INCLUDE to point at a reusable file.

SIDELOAD — the body travels with the job
SCHEDULE [SIDELOAD `
NEW OPERATION report
OPEN
  MAIL TEXT BODY "The scheduled report ran" TO "admin@example.com" SUBJECT "Report"
CLOSE
AFTER RUN report
`] AT ?dateandtime SET ?job

INCLUDE is the alternative when the same body is shared by several jobs — put the definition and the call together in one .oql file in your namespace:

/root/ops/report.oql
NEW OPERATION report
OPEN
  EMIT "Ran at the scheduled time"
CLOSE
AFTER RUN report
Which to use. 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 with the reason.

SCHEDULE — Run Once, at a Future Moment

Schedule an Operation
SCHEDULE [INCLUDE "/root/ops/report.oql"] AT ?dateandtime SET ?job

CRON — Run Repeatedly, on an Interval

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 and EVERY 9 SECONDS both behave as roughly ten. Intervals are also a minimum, not a grid — the next run is scheduled from the moment the previous one finished, so a job drifts progressively later rather than holding the clock.

Every Hour, Starting Immediately
NEW OPERATION opname
OPEN
  EMIT "Hourly run"
CLOSE
AFTER NEW CRON [INCLUDE "/root/ops/report.oql"] EVERY 1 HOUR STARTING NOW SET ?job
Every Two Hours, Starting at a Specific Time
NEW DATE AND TIME SET ?dateandtime
AFTER NEW OPERATION opname
OPEN
  EMIT "Bi-hourly run"
CLOSE
AFTER NEW CRON [INCLUDE "/root/ops/report.oql"] EVERY 2 HOURS STARTING ?dateandtime SET ?job

The Triggered Expression Is Not Limited to Operations

The bracket takes one statement, and any statement will do — [CALCULATE 1 + 1], [FETCH "url"], [MAIL TEXT BODY "hi" TO "a@b.com" SUBJECT "x"]. An AFTER chain inside the bracket does not parse; that is what the .oql file is for.

A Simple Inline Expression as the Job Body
SCHEDULE [CALCULATE 1 + 1] AT ?dateandtime SET ?job

Registry — List, Delete, Describe

Every scheduled and cron job is stored in a persistent registry — listable, deletable, and describable at any time, from any script.

SCHEDULE Registry
SCHEDULE LIST SET ?jobs
AFTER FOREACH ?jobs SET ?key AS ?job
OPEN
SCHEDULE DESCRIBE ?job SET ?info
AFTER EMIT ?info & "\n"
CLOSE
SCHEDULE Registry — Delete Every Job
SCHEDULE LIST SET ?jobs
AFTER FOREACH ?jobs SET ?key AS ?job
OPEN
SCHEDULE DESCRIBE ?job SET ?info
AFTER SCHEDULE DELETE ?job
CLOSE
CRON Registry
CRON LIST SET ?jobs
AFTER FOREACH ?jobs SET ?key AS ?job
OPEN
CRON DESCRIBE ?job SET ?info
AFTER EMIT ?info & "\n"
CLOSE
CRON Registry — Delete Every Job
CRON LIST SET ?jobs
AFTER FOREACH ?jobs SET ?key AS ?job
OPEN
CRON DESCRIBE ?job SET ?info
AFTER CRON DELETE ?job
CLOSE

Lifecycle, Status and Errors

A SCHEDULE fires once and is then marked done. The row is never removed — it keeps its status and its error, and deleting it is your call. CRON repeats by definition and is never marked done. LIST and DESCRIBE return done, status (pending, ok or failed) and, on DESCRIBE, last_error with the failure message verbatim. A completed one-shot reports next_due: null.

Every run spends a query. A job firing is an ordinary API call made in your name: it runs in your namespace, against your daily query allowance, and increments your usage exactly as if you had made the request yourself.
Missed windows are not replayed. A job due while the scheduler is down runs once when it comes back and re-schedules from that moment; the windows in between are not made up. Each run is allowed the full OcaltQL script ceiling of 300 seconds, and jobs execute in parallel, so a slow one does not hold up anyone else's.
Internal architecture. Every registered job is stored in a persistent registry inside ocalt-core, the same infrastructure already backing subdomains and quota. A single master scheduler loop inside ocalt-core reads this registry continuously, and when a job comes due, calls back into ql-runtime’s credentialed API internally, authenticated as that specific job’s own owner — the same internal-authentication shape already used by Site Mode. The script that registered the job does not need to still be running, or even exist, for it to fire.