Skip to content

ScheduledFunction CRD

The ScheduledFunction custom resource fires a function-mode AgentRuntime on a cron schedule, optionally carrying a cursor from one fire into the next.

There is no new deployable and no per-fire Kubernetes object. The operator dispatches over HTTP with its own ServiceAccount token, and each fire’s history is the ordinary sessions row the facade already writes.

apiVersion: omnia.altairalabs.ai/v1alpha1
kind: ScheduledFunction

Short name: sf (e.g. kubectl get sf).

Only function-mode AgentRuntimes can be scheduled — the target is invoked at POST /functions/{name}, so a conversational agent has no schedulable surface.

apiVersion: omnia.altairalabs.ai/v1alpha1
kind: ScheduledFunction
metadata:
name: hourly-digest
namespace: my-workspace
spec:
functionRef:
name: digest-fn
schedule:
cron: "@hourly"
runAs:
systemUser: {}

The function-mode AgentRuntime to fire, in the same namespace.

Field Type Required
name string Yes
Field Type Default Description
cron string — (required) Five-field cron expression (0 6 * * *) or a descriptor: @hourly, @daily, @weekly, @monthly, @yearly, @every <duration>
timezone string UTC IANA zone the expression is evaluated in
concurrencyPolicy enum Forbid Allow or Forbid

The controller’s parse is the source of truth for what counts as a valid schedule — the CRD deliberately carries no pattern constraint, because one that admitted every descriptor also admitted nonsense.

concurrencyPolicy: Replace does not exist. An in-flight HTTP invocation cannot be cancelled; abandoning the response still leaves the runtime spending provider tokens to completion.

Declares which slice of a fire’s output is carried into the next fire.

Field Type Required Description
cursorFrom string Yes JSON Pointer (RFC 6901) into the fire’s output
cursorInto string Yes JSON Pointer into the next fire’s input
initial JSON No Cursor value used before the first successful fire
store object Yes Where the cursor is kept

If cursorFrom resolves to nothing on an otherwise successful fire, the stored cursor carries forward unchanged — a function that returned no new cursor has not asked to forget where it was.

initial accepts a scalar ("2026-01-01T00:00:00Z", 0, "page-2") as well as an object. The useful cursor shapes are mostly scalars.

Exactly one provider key may be set. Only configMap is implemented; the map shape admits others without a breaking change.

Field Type Default Description
configMap.name string <schedule-name>-state ConfigMap holding the cursor
configMap.key string cursor Key within its data

A ConfigMap the controller creates from the default name is owned by the schedule and garbage-collected with it. One you name explicitly is adopted, written to, and left behind on delete — so state can outlive a recreate on purpose.

Field Type Description
systemUser object Fire as a stable per-schedule principal, system:schedule:<workspace>/<name>

systemUser is the only arm in v1. The union stays open for user and credentialRef.

Because the principal is stable per schedule, agentic memory continuity falls out for free: successive fires are the same end user, so a schedule can remember across runs without any additional field.

Field Type Default Description
suspend boolean false Stop firing without deleting the object
startingDeadlineSeconds int32 Skip a fire that is later than this past its slot
timeoutSeconds int32 Abandon a fire that runs longer than this
input object The frozen parameter set, validated against the function’s inputSchema before any fire. The cursor is written into it at state.cursorInto
Field Description
nextFireTime When the schedule fires next
lastScheduleTime The slot the last fire belonged to
lastFireTime When the last fire was dispatched
lastSuccessTime When a fire last succeeded
lastOutcome Succeeded, Failed, TimedOut or Unknown
lastInvocationID Correlates with the session row for that fire
lastSkipReason Why a due tick did not fire
consecutiveFailures Reset on success
state Where the cursor lives — provider, ref and updatedAt. Never the value itself
inFlight Marker for a dispatch in progress
Reason Meaning
Suspended spec.suspend is true
MissedDeadline The tick was later than startingDeadlineSeconds
ConcurrencyForbidden A fire was still in flight under Forbid
DispatchCapReached The operator’s global in-flight dispatch cap was full
DispatchStartFailed The dispatch could not be started

DispatchCapReached is a global operator cap, not a per-workspace quota. It protects the operator from one workspace’s fan-out saturating dispatch and slowing every other reconciler. A fire that cannot acquire a slot is skipped, never queued — a backlog of stale fires is worse than a missed one.

status holds scalars only. The cursor’s value lives in the state store, never on the object — status.state is a pointer to it, so a cursor carrying a customer identifier does not end up readable in kubectl get sf -o yaml.

The cursor advances only on HTTP 200 with schema-valid output. There are no retries within a period: the next tick is the retry.

Terminal window
$ kubectl get sf
NAME FUNCTION SCHEDULE SUSPENDED LAST NEXT
hourly-digest digest-fn @hourly false Succeeded 2026-08-22T15:00:00Z