Schedule functions
Fire a function-mode AgentRuntime on a cron schedule with fixed input, for recurring work that has no caller: a nightly digest, an hourly sync, a periodic sweep of a queue.
Every spec field, status field, skip reason, condition and metric is in the ScheduledFunction reference.
Prerequisites
Section titled “Prerequisites”- A
mode: functionAgentRuntime in the workspace namespace, withspec.facades[].managementPlaneenabled. The operator dispatches to the function’s management-plane endpoint, so a schedule on a function without one reportsSpecValid=Falsewith reasonManagementEndpointMissing.
Create the schedule
Section titled “Create the schedule”-
Apply a
ScheduledFunctionin the same namespace as the function:apiVersion: omnia.altairalabs.ai/v1alpha1kind: ScheduledFunctionmetadata:name: daily-digestnamespace: ws-acmespec:functionRef:name: daily-digestschedule:cron: "0 6 * * *"timezone: "Europe/London"concurrencyPolicy: Forbidinput:region: emearunAs:systemUser: {}inputis validated against the function’sinputSchemabefore any fire. Each fire is recorded as an ordinarysessionsrow taggedfunction. -
Set
suspend: falseif the operator may run with--hold-unreviewed-schedules. See Holding schedules nobody has reviewed.
Carry state between runs
Section titled “Carry state between runs”Skip this section if each run is independent. Otherwise declare a cursor: a slice of one fire’s output that Omnia splices into the next fire’s input.
-
Add
spec.state:spec:input:region: emeastate:cursorFrom: "/cursor" # JSON Pointer into the fire's outputcursorInto: "/cursor" # JSON Pointer into the next fire's inputinitial: # used before the first successful firesince: "2026-01-01T00:00:00Z"store:configMap: {} # defaults to a ConfigMap named <schedule>-state -
Make the function read
cursorfrom its input and return a newcursorin its output. With the example above it receives{"region":"emea","cursor":{"since":"..."}}.
Keep the cursor to a resumption pointer: a watermark timestamp, a last-processed id or a page token. If the store is lost, the next fire reprocesses a window, so never keep the only copy of a business fact in it. A cursor over 64 KiB fails the fire.
The cursor advances only when the fire returns HTTP 200 with output that
validates against the function’s outputSchema. After any other outcome the
stored cursor is unchanged and the next fire retries the same window. A fire
whose cursor could not be saved is recorded as Failed.
Use Forbid for any schedule that carries a cursor. Under Allow, overlapping
fires can move the cursor backwards.
If the function has memory enabled, each schedule
runs as the stable principal system:schedule:<workspace>/<name>, so what one
fire writes the next can recall.
Make the function repeatable within a fire slot
Section titled “Make the function repeatable within a fire slot”In a DST-observing timezone, a schedule whose slot falls in the repeated hour
fires twice on fall-back day, and one whose slot falls in the skipped hour does
not fire on spring-forward day. A watermark-driven function absorbs the second
fire as an empty window. A function with non-idempotent side effects, such as
sending email or writing to a CRM, must be made idempotent within a fire slot,
or the schedule must use timezone: UTC.
Holding schedules nobody has reviewed
Section titled “Holding schedules nobody has reviewed”An unset suspend fires by default. When the operator runs with
--hold-unreviewed-schedules, set from the chart value
scheduledFunctions.holdUnreviewed: true, schedules with suspend unset are
held with the skip reason SuspendedByDefault. Enable it where applications
are installed and tried out, so an install does not start recurring spend
against a paid provider unnoticed. State that a schedule should run:
spec: suspend: falseRun a schedule now
Section titled “Run a schedule now”Set the trigger annotation to any new value. The dashboard’s Run now does the same:
kubectl annotate scheduledfunction daily-digest -n ws-acme \ omnia.altairalabs.ai/trigger-now="$(date -u +%FT%TZ)" --overwriteA manual fire is a real fire: it reads and advances the cursor, writes the same
session row and obeys concurrencyPolicy. It runs once per annotation value. A
suspended schedule consumes the value without firing, so unsuspending does not
replay it. If the operator’s dispatch cap is full (DispatchCapReached), the
request stays pending and is retried on the next reconcile; see the
dispatch cap.
To invoke the function without touching stored state, use the function’s test panel in the dashboard.
Check a schedule
Section titled “Check a schedule”kubectl get scheduledfunction -n ws-acmeThe columns include READY, LAST (the last fire’s outcome) and NEXT. For
detail, read the status:
kubectl get scheduledfunction daily-digest -n ws-acme -o yamlstatus.lastOutcomeandstatus.consecutiveFailuressay how fires are going.status.lastSkipReasonsays why the last due tick did not fire.status.statepoints at where the cursor lives. It never holds the cursor value; read the ConfigMap for that.
The reference describes the
status fields, including the SpecValid and Ready conditions and the
timestamps. The omnia_scheduled_function_* metrics are listed under
Metrics.
Troubleshooting
Section titled “Troubleshooting”The schedule never fires
Section titled “The schedule never fires”Read status.conditions first.
SpecValid=Falsenames the spec problem: a missingfunctionRef,inputthat fails the function’sinputSchema, a malformed cursor pointer, anoutputSchemathat will not compile, orManagementEndpointMissing. The last one means the function’s facade hasmanagementPlanedisabled.Ready=FalsewithDispatchStartFailedmeans the tick was due but the dispatch could not start, for example because the cursor ConfigMap is unreadable. The cause is in the condition message and the skip event.
If both conditions are True, check status.nextFireTime and
status.lastSkipReason. The reasons are listed in the
reference.
Ready=False with CronInvalid
Section titled “Ready=False with CronInvalid”The controller parses spec.schedule.cron. It accepts standard five-field
expressions and the descriptors @hourly, @daily, @weekly, @monthly,
@yearly and @every <duration>.
lastOutcome: Unknown
Section titled “lastOutcome: Unknown”The operator restarted while the fire was in
flight, so its result was never observed. The cursor did not advance, so the
next fire retries the same window. Unknown does not increment
consecutiveFailures.