Skip to content

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.

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.

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>
}

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:

  • handlerName is the function name (dailyReport). It must be a valid JS identifier and must match the name the Builder registered via set_schedule.
  • registerDailyReport is the registration vehicle: it exists so sdk.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 registerDailyReport is 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 the handlerName must match the DB row.
  • No sdk inside 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 reference sdk.

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): void

spec 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.

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 as input.payload when 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 fires
export async function sendDigest(input: { scheduledAt: string; payload?: unknown }) {
// process one unit of work
}
export function registerSendDigest(sdk: AppSdk) {
sdk.scheduling.cron("* * * * *", sendDigest)
}

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.

ErrorWhen it is thrown
InvalidHandlerErrorThrown synchronously by cron when the spec is not a valid 5-field expression, or the handler has no usable .name.
NotAvailableInV1ErrorThrown by enqueue while the platform queue producer is unwired — which is the case today. Carries capability for context.
  • Handlers always receive { scheduledAt; payload? }. scheduledAt is an ISO 8601 timestamp. payload is whatever was passed to enqueue, and is absent on cron-trigger invocations.
  • Cron is for entry points; enqueue is for fan-out. The two primitives are complementary, not alternatives — use cron to start time-based work and enqueue to distribute the heavy units inside it.
  • All schedules are UTC. A spec is 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.