Scheduling
Scheduling is coming-soon: the Builder now generates handler files (gap #1 closed), but the platform queue-consumer dispatch path that actually fires handlers is still being wired (gap #2). In practice that means sdk.scheduling.enqueue throws NotAvailableInV1Error today, and registered cron handlers survive K27 cross-check and are deployed in the Worker bundle, but are not yet invoked end-to-end on the platform. Read this page for the contract you will build against; do not expect a scheduled job to actually fire yet.
Scheduling gives your App’s handlers two primitives for running work outside the normal request/response cycle:
- cron — register a named function to fire automatically on a UTC schedule.
- enqueue — push a payload onto your App’s async queue for a registered handler to process later.
Your App code always calls sdk.scheduling.*. The platform owns the cron-trigger and queue infrastructure underneath; you write named async functions and register them. There is no provider to choose and no provider keys to set — Scheduling uses a single platform queue backend.
Enabling Scheduling
Section titled “Enabling Scheduling”Scheduling is turned on per App through the plan, not a plan flag. When you describe time-based work to the Builder — phrasing like “nightly”, “hourly”, “weekly”, “scheduled”, “cron”, or any long-running job that must not block a request — the Builder enables Scheduling for the App and registers each schedule for you. You do not call the registration tool yourself; the Builder does it during generation.
The SDK surface
Section titled “The SDK surface”type ScheduledHandlerInput = { scheduledAt: string; payload?: unknown }type ScheduledHandler = (input: ScheduledHandlerInput) => Promise<void> | void
type EnqueueOpts = { idempotencyKey?: string delaySeconds?: number maxRetries?: number}
type SchedulingCapability = { cron(spec: string, handler: ScheduledHandler): void enqueue(name: string, payload: unknown, opts?: EnqueueOpts): Promise<void>}Handler file shape
Section titled “Handler file shape”Each cron handler lives in src/handlers/<handlerName>.ts and must follow the register-function dual-export shape:
import type { AppSdk } from "@anyknown/app-server"
// Named export — the function the platform imports and calls when the schedule fires.// The handler body has no `sdk` in v1; it receives only { scheduledAt, payload? }.export async function dailyReport(input: { scheduledAt: string; payload?: unknown }) { // validate/normalize the payload, return data, or throw for a fatal failure}
// Never-invoked register function — exists only to make the registration typed// and scanner-visible. The platform never calls registerDailyReport at runtime.export function registerDailyReport(sdk: AppSdk) { sdk.scheduling.cron("0 9 * * *", dailyReport)}Rules:
handlerNameis the function name (dailyReport). It must be a valid JS identifier and must match the name the Builder registered viaset_schedule.registerDailyReportis the registration vehicle: it exists sosdk.scheduling.cron(...)is valid TypeScript (sdk is the typed parameter) and so the source scanner can find the cron expression. It is never invoked.- The cron literal in
registerDailyReportis cosmetic. The platform dispatches on the cron expression stored at registration time — not the one in the source file. A drifted in-file cron does not cause staleness; only thehandlerNamemust match the DB row. - No
sdkinside the handler body in v1. The platform passes{ scheduledAt, payload? }when the handler fires; sdk injection is planned but not yet wired (gap #2). The handler body may validate or transform the input and return a value; it must not referencesdk.
Registering a cron handler
Section titled “Registering a cron handler”cron registers a named function to fire on a UTC schedule. It appears inside the register<Name> function (never at module scope) and returns void.
cron(spec: string, handler: ScheduledHandler): voidspec is a 5-field UTC cron expression: minute, hour, day-of-month, month, day-of-week. For example "0 9 * * *" (09:00 UTC daily), "*/15 * * * *" (every 15 minutes), "0 0 * * 1" (Mondays at midnight UTC). Six-field (seconds) and Quartz-style extensions (?, L, W, #) are not supported.
When the schedule fires, your handler receives { scheduledAt }. There is no payload on a cron-trigger invocation — payload is only present when a handler is reached through enqueue.
Enqueuing async work
Section titled “Enqueuing async work”enqueue pushes a payload onto your App’s async queue, targeting a registered handler by name. It is the tool for fan-out within a handler: a cron fires a coordinator handler, and that handler enqueues many small units of work so the cron itself stays fast.
enqueue(name: string, payload: unknown, opts?: EnqueueOpts): Promise<void>name— the name of the handler that should process this item. It must exactly match a registered handler’s function name.payload— any serializable value; it arrives asinput.payloadwhen the handler fires.opts(all optional):idempotencyKey?: string— deduplicate queue entries by key.delaySeconds?: number— defer execution by N seconds.maxRetries?: number— override the platform default retry count.
import type { AppSdk } from "@anyknown/app-server"
export async function nightlyDigest(input: { scheduledAt: string; payload?: unknown }) { // v1: no sdk in handler body yet — validate input, structure work, return return { ok: true }}
export function registerNightlyDigest(sdk: AppSdk) { sdk.scheduling.cron("0 2 * * *", nightlyDigest)}
// A separate enqueue target — the handler processed when a unit of work firesexport async function sendDigest(input: { scheduledAt: string; payload?: unknown }) { // process one unit of work}
export function registerSendDigest(sdk: AppSdk) { sdk.scheduling.cron("* * * * *", sendDigest)}The handler-name contract
Section titled “The handler-name contract”The function name is the routing key end to end. The name the Builder registers, the name you pass to enqueue, and the exported async function in your App source must all match exactly. A mismatch means the platform cannot route the invocation, and the item is never delivered.
Errors
Section titled “Errors”| Error | When it is thrown |
|---|---|
InvalidHandlerError | Thrown synchronously by cron when the spec is not a valid 5-field expression, or the handler has no usable .name. |
NotAvailableInV1Error | Thrown by enqueue while the platform queue producer is unwired — which is the case today. Carries capability for context. |
Things to know
Section titled “Things to know”- Handlers always receive
{ scheduledAt; payload? }.scheduledAtis an ISO 8601 timestamp.payloadis whatever was passed toenqueue, and is absent on cron-trigger invocations. - Cron is for entry points;
enqueueis for fan-out. The two primitives are complementary, not alternatives — use cron to start time-based work andenqueueto distribute the heavy units inside it. - All schedules are UTC. A
specis interpreted in UTC; there is no per-App time zone. - An App may have at most 10 active schedules. This is a hard platform ceiling. If you need more time-based jobs than that, consolidate related work into a single handler with conditional logic rather than adding schedules.