Scheduling
@Cron marks a DI-managed async method to run on a schedule. The application-facing decorator is the same in local processes and wasmCloud deployments. Locally, the container starts in-process timers when the service is resolved. In a wasmCloud deployment the plugin disables those timers and Kubernetes CronJobs privately invoke the method.
There is no di-framework cron command. Discovery, invoker generation, and CronJob manifests are part of di-framework wasmcloud build/deploy.
Installation
Cron is also exported from @di-framework/core/cron and @di-framework/core.
Declare a scheduled method
The scheduled-worker example runs two jobs (nightly-prune at 02:00 and partition-rebalance every 15 minutes) with "ingress": false in di-framework.config.json.
Schedule expressions
@Cron(schedule, options?) accepts:
A 5-field cron string:
minute hour dayOfMonth month dayOfWeekA numeric interval in milliseconds
Supported field syntax: *, */N, N,M, N-M, and exact N. Anything other than five space-separated fields throws:
Not supported: seconds / 6–7 fields, @hourly/@daily, ?, month or day names (MON), CRON_TZ=…, Sunday-as-7.
There is no timeZone option. In-process matching uses the process local Date fields, not UTC and not an IANA zone. Generated Kubernetes CronJobs use the cluster controller timezone.
Options
Field | Default | Behavior |
|---|---|---|
|
| Stable job id used by |
|
| Overlapping |
| — | Human-readable only; unused at runtime |
| — | Max wait on |
Jobs register when the owning service is resolved. allowConcurrent and timeoutMs apply to the invoke path, not to in-process setInterval/setTimeout timers.
Numeric intervals in deployment
Locally, @Cron(30000) is a 30-second setInterval. For external schedulers the expression is normalized to whole minutes:
Sub-minute intervals are rounded, not rejected. @Cron(30000) becomes every minute on wasmCloud.
In-process versus external mode
CronMode is 'in-process' | 'external'.
Default:
process.env.DI_CRON_MODE === 'external' ? 'external' : 'in-process'container.setCronMode(mode)must run beforeresolve, or timers already startedcontainer.getCronMode()/container.isExternalCron()
In-process: numeric schedules use setInterval; cron strings chain setTimeout (next fire is at least one minute after now). Errors log [Cron] Class.method threw and the schedule continues. Multiple processes or replicas each fire independently.
External: the job is still registered, but no in-component timers start. wasmCloud sets DI_CRON_MODE=external on the workload whenever jobs are discovered, and the generated invoker calls container.setCronMode('external') before invokeCronJob.
Exactly-once execution is not promised. External mode is “Kubernetes fires, the component runs the method if reachable,” plus skip-on-overlap when allowConcurrent is false.
Manual invocation and tests
There is no controllable cron clock. Tests call invokeCronJob instead of waiting for timers. CronRuntime is a process singleton; tests should call CronRuntime.reset() in beforeEach/afterEach.
CronExecutionResult fields: jobId, status (success | failure | skipped), success, startedAt, completedAt, durationMs, optional result/error/reason.
Default: failures return
status: 'failure'(no throw){ throwOnError: true }throwsCronExecutionErrororCronConcurrencyErrorUnknown id always throws
CronJobNotFoundError(lists available ids)Concurrent invoke with
allowConcurrent: falsereturnsstatus: 'skipped'
The scheduled method is always called with zero arguments. CronInvocationContext (timestamp, source, metadata) is unused by the runtime.
Also available: container.getCronJobs(), container.stopCronJobs(), CronRuntime.current.getStatusReports(). container.clear() stops timers and drops registrations (the tested reload path). ApplicationContext.stop() calls stopCronJobs().
There is no runtime API to change a schedule. Edit @Cron, rebuild, and redeploy.
Scheduled-only applications
http: false is treated the same as ingress: false.
When jobs exist, the wasmCloud plugin still exports wasi:http/[email protected] and still emits a ClusterIP Service on port 80 so CronJobs can POST to /_di/cron/{jobId}/invoke. Public ingress (HTTP URL/host in the deploy result) is omitted. The application fetch handler is not required for scheduling; control routes are intercepted before the app handler.
The default export must expose the DI container:
wasmCloud CronJobs
di-framework wasmcloud build discovers @Cron(...) calls with a string or numeric literal under src/ (otherwise the project root). Dynamic schedules are skipped. Duplicate jobId values keep the first file.
Deploy applies one Kubernetes batch/v1 CronJob per job:
Name
{witName}-{kebab-job-id}concurrencyPolicy: ForbidorAllowfromallowConcurrentcurlPOST tohttp://{name}.{ns}.svc.cluster.local/_di/cron/{jobId}/invokewithAuthorization: Bearerfrom the workload control secretDefault invoke timeout 30s when
timeoutMsis omittedWorkload
spec.replicas: 1
di-framework wasmcloud destroy deletes WorkloadDeployment,service,cronjob labeled app.kubernetes.io/name=<witName>. Redeploy kubectl applys the regenerated manifest; jobs removed from source are not pruned except via destroy.
Deployed HTTP workloads always receive DI_CONTROL_TOKEN. Unconfigured local/dev may invoke without a token, but never administer. Control paths are not reachable through public ingress. See Control HTTP.
A failed or skipped CronExecutionResult (or a thrown invoke) returns HTTP 500 with a generic Cron job failed body so the Kubernetes job does not record success. The response does not echo error.message.
di-framework wasmcloud dev serves locally and does not generate Kubernetes CronJobs. doctor does not check cron configuration.
Overlap, retries, missed runs
Concern | Implemented behavior |
|---|---|
Application-wide vs per-replica | Generated workloads use |
Overlap |
|
Timeouts |
|
Retries | No application-level retry option. Kubernetes Job |
Missed runs | No catch-up. In-process next fire is computed from now. |
Restart | After a skipped overlap, a later invoke succeeds. |
Troubleshooting
Symptom | Cause |
|---|---|
Job never fires locally | Service not resolved; or |
Duplicate fires in deployment | In-process timers still running; confirm |
|
|
Invalid cron expression | Not five fields |
wasmCloud skips a job | Non-literal |
HTTP 500 on invoke | Job returned |
Next steps
Advanced Usage - Container patterns used by scheduled services
Testing - Isolated containers and
CronRuntime.reset()wasmCloud - Build and deploy the generated CronJobs
CLI - Canonical command tree (cron is not a built-in group)