0621 — a schedule is a producer on a cron expression; "cron job" is renamed "schedule" everywhere
Edit this pageAmends [0468], [0471], [0520], [0531]; the noun applies on top of [0611]/[0613].
Context
MessageAdmin.RegisterCronJob stores an any payload and the fleet
scheduler produces a cron.JobRequest envelope onto
__system.job_requests with the job’s name as routing key. A user
consuming their own job must know the system topic constant, that
name == binding, and unmarshal JobRequest.Payload by hand; nothing
types the two ends. The register/consume shape is also unlike the
other two handles (NewProducer / NewConsumer -> Register[T] ->
verb). Two false starts were rejected in design: a handler on the job
config (couples the consumer to the schedule), and a payload func run
by a per-job loop in the user’s process (nothing fires unless that
process runs — the same coupling from the other side). The spec is
website/src/content/docs/guides/schedules.mdx.
Decision
- A schedule is a producer on a cron expression. The system produces
the stored message onto the user’s own topic; consumers are ordinary
consumer.Register[T]groups on that topic and know nothing about the schedule. - Third API handle, the producer/consumer mirror:
scheduler.NewScheduler(ds, cfg)->Register[Message topic.Versioned](ctx, name, expression, topicName, payload *Message, cfg)returns*scheduler.SchedulerInstance[Message]->Schedule(ctx)runs the system manager (nothing per-schedule; it exists so a register-and-exit program is a manager somewhere). Registerstores the marshaled payload andMessage’s schema version on the row; the fleet worker produces that JSON at that version — the append path takes the row’s version instead of computing it from a type.topic_idis the target topic, no longer an owner column. Message key = the schedule name, compaction on, concurrency/timeout from config, idempotency key from (scheduled time, schedule id) — the produce mechanics of [0463]/[0471] are unchanged.scheduled_atreaches the handler throughconsumergroup.MessageMeta, never the payload.- The resource is named “schedule” on every surface: domain package
pkg/schedule(read-modelschedule.Schedule, the parsed cron stringschedule.Expression), tablesschedule_config/schedule_config_log/schedule_cursorwith columnexpression, adminRegisterSchedule/RunSchedule/SuspendSchedule/UnsuspendSchedule/ScheduleStatus/ScheduleMessages/DestroySchedule, CLIvulkan schedule ..., log attributesschedule/schedule_id,ErrScheduleNotFound. The worker becomespkg/schedule/producer. cron.JobRequestandcron.TopicNameleave the user surface; the built-in alerts keep__system.job_requestsas their own target topic and register through the same handle. Amended 2026-08-30 at ship: the alerts’ target topic isschedule.TopicName=__system.schedules, andMessageAdminregisters their schedules through its ownScheduler— admin has no RegisterSchedule verb.
Consequences
- One noun on code, admin, CLI, tables, and logs — the code/CLI split (“Schedule” in Go, “cron” on the command line) was the tell that forced the rename. Temporal and EventBridge Scheduler name the same resource “Schedule”; K8s keeps “CronJob” because what it fires is a batch job, not a message.
- A schedule’s name shares the target topic’s message-key space with the user’s own keys; documented, no config knob until someone hits it.
- Rejected: package
cronjob(handle collides with the read-model), packageschedulefor the API handle (collides with the expression type), pluralNewSchedulers, topic-firstRegisterargument order, and both coupled shapes named in Context.