Add recurring work#
Recurring work schedules an already registered background job from a source-controlled UTC cron definition. Use a one-off job with .after or .at instead when the schedule belongs to one application event.
Choose the schedule contract#
Decide the stable schedule name, UTC expression, target job, arguments, and the handler's behavior when an occurrence is delayed or coalesced after downtime.
Implement#
Define the work as a job callback in src/jobs.zig, register it, and then add its schedule. This example deletes expired rows from an application-owned activity_events table with an expires_at timestamptz column. Create the table through an application migration before running the job.
Add these entries to any existing registries rather than declaring a second registry or recurring:
const zyn = @import("zyn");
fn cleanup(job: *zyn.Job, _: struct {}) !void {
try job.db.exec(
"DELETE FROM activity_events WHERE expires_at <= now()",
.{},
);
}
pub const registry: zyn.JobRegistry = &.{
.job("cleanup", cleanup, .{}),
};
pub const recurring: zyn.RecurringJobRegistry = &.{
.cron(
"nightly_cleanup",
"15 0 3 * * *",
"cleanup",
.{},
),
};
The schedule enqueues cleanup every day at 03:00:15 UTC. For a parameterized job, supply the callback's argument fields in the schedule.
Database errors follow the worker's retry policy. Repeating this deletion is safe, and a delayed occurrence still removes all currently expired rows.
The schedule name must be unique strict snake case. The expression, name, and target are compile-time strings; the target names a registered job, and the arguments must match that job's argument type.
Cron fields are UTC. Six fields represent second, minute, hour, day of month, month, and day of week. Five fields are accepted with second 0. Time zones, macros, interval schedules, and per-occurrence catch-up are not available.
Source definitions are authoritative. A changed definition starts from its next future occurrence. An unchanged definition with missed times materializes only the latest missed occurrence, not one job for every interval. The target handler must therefore tolerate delay, coalescing, retries, and duplicate execution.
Do not rename a recurring definition casually: its name identifies its stored cursor. Treat a rename as removing one schedule and creating another.
Verify#
zyn jobs list
zyn jobs recurring
zyn check
Run zyn dev to start PostgreSQL, HTTP, and the job worker together, or zyn jobs work to run only the worker with PostgreSQL available. zyn serve runs HTTP without a worker; listing jobs or recurring schedules does not execute them either.
In a managed application test, use app.jobs.enqueue_recurring with an explicit PostgreSQL time, then execute eligible work separately. Test boundary times, idempotent repeated synchronization, and any business rule affected by missed occurrences.
Check the name, expression, and target with zyn jobs recurring.