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.
API version
Section titled “API version”apiVersion: omnia.altairalabs.ai/v1alpha1kind: ScheduledFunctionShort 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.
Minimal example
Section titled “Minimal example”apiVersion: omnia.altairalabs.ai/v1alpha1kind: ScheduledFunctionmetadata: name: hourly-digest namespace: my-workspacespec: functionRef: name: digest-fn schedule: cron: "@hourly" runAs: systemUser: {}Spec fields
Section titled “Spec fields”functionRef
Section titled “functionRef”The function-mode AgentRuntime to fire, in the same namespace.
| Field | Type | Required |
|---|---|---|
name |
string | Yes |
schedule
Section titled “schedule”| 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.
state.store
Section titled “state.store”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.
Other fields
Section titled “Other fields”| 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 |
Status fields
Section titled “Status fields”| 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 |
Skip reasons
Section titled “Skip reasons”| 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.
Cursor advance semantics
Section titled “Cursor advance semantics”The cursor advances only on HTTP 200 with schema-valid output. There are no retries within a period: the next tick is the retry.
Printed columns
Section titled “Printed columns”$ kubectl get sfNAME FUNCTION SCHEDULE SUSPENDED LAST NEXThourly-digest digest-fn @hourly false Succeeded 2026-08-22T15:00:00ZRelated
Section titled “Related”- Schedule a function — task guide
- AgentRuntime CRD —
mode: function