Problem / Goal
server/services/autonomousJobs/ (app custom scheduled tasks) offers recurring intervals only. Quota Burn needs to invoke a custom task on demand without it also firing on a clock, and users need to author "run only when I ask" custom tasks. Today the only way to fake this is to create a weekly job and disable it — which also makes it un-runnable.
Add an explicit on-demand cadence as a first-class option across schema, CRUD, due/next-due calculation, timer registration, and the UI.
Note this enum is the autonomous-job interval vocabulary (hourly | every-2-hours | every-4-hours | every-8-hours | daily | weekly | biweekly | monthly | custom). It is a separate vocabulary from the scheduled-CoS-task INTERVAL_TYPES = { ON_DEMAND, CRON } in server/services/taskScheduleConstants.js, and nothing converts between them — do not merge them here.
Scope
server/services/autonomousJobs/constants.js — add the cadence to INTERVAL_OPTIONS and make resolveIntervalMs(interval, customMs) (line 31) return an explicit no-interval sentinel (null) for it. Today the switch ends in default: return DAY, so an unrecognized or on-demand value silently becomes a daily job. That fall-through is the core bug this issue must close.
Every resolveIntervalMs consumer must handle the sentinel explicitly — arithmetic on null yields NaN / Invalid Date:
server/services/autonomousJobs/store.js:190 (migrateScriptsState)
server/services/autonomousJobs/crud.js:95 (createJob) and :169 (updateJob)
client/src/utils/cronHelpers.js — JOB_INTERVAL_OPTIONS (line 287) is the hand-maintained client mirror of the server list and must gain the same row
Due / next-due — there are three separate implementations, all of which need the sentinel:
server/services/autonomousJobs/scheduler.js#getDueJobs (line 36) and #computeNextJobRun (line 108)
server/services/cosJobScheduler.js#computeNextJobFireTime (line 52) — this is the one that actually arms the timer via registerSingleJobSchedule / registerJobSchedules, not scheduler.js. An on-demand job must register no timer while staying enabled.
Validation — createCosJobSchema (server/lib/cosValidation.js:406) declares interval: z.string().optional(), a bare free-form string, so a typo'd cadence already reaches disk and silently reschedules daily. Tighten it to the cadence enum (including the new value) so the boundary rejects it; updateCosJobSchema (:464) inherits via .partial(). intervalMs must not be required or back-filled for the on-demand cadence.
UI — client/src/components/apps/tabs/CustomTasksSection.jsx owns the form state and toPayload, but the cadence widgets are ScheduleFields, exported from client/src/components/cos/JobCard.jsx (line 91; the <select> bound to data.interval is at 133, cadence label at 376, due computation at 160 and 330). Offer the cadence, hide/disable the interval-only inputs when it is selected, and render next-run as "On demand" rather than a date. GET /api/cos/jobs/intervals (server/routes/cosJobRoutes.js) serves INTERVAL_OPTIONS verbatim, so the new row flows through automatically.
Preserve existing recurring cadences and job:spawned bookkeeping exactly. Do not reintroduce a per-app quota-burn scheduled task type.
Acceptance criteria
Notes
Independent of the rest of #6372 and safe to ship first. Children that reference custom tasks (the migration and the picker UI) depend on this cadence existing.
Part of #6372
When this ships, tick its box in the ## Decomposed into checklist on #6372. If it is the last unchecked box, close #6372.
Problem / Goal
server/services/autonomousJobs/(app custom scheduled tasks) offers recurring intervals only. Quota Burn needs to invoke a custom task on demand without it also firing on a clock, and users need to author "run only when I ask" custom tasks. Today the only way to fake this is to create a weekly job and disable it — which also makes it un-runnable.Add an explicit on-demand cadence as a first-class option across schema, CRUD, due/next-due calculation, timer registration, and the UI.
Note this enum is the autonomous-job interval vocabulary (
hourly | every-2-hours | every-4-hours | every-8-hours | daily | weekly | biweekly | monthly | custom). It is a separate vocabulary from the scheduled-CoS-taskINTERVAL_TYPES = { ON_DEMAND, CRON }inserver/services/taskScheduleConstants.js, and nothing converts between them — do not merge them here.Scope
server/services/autonomousJobs/constants.js— add the cadence toINTERVAL_OPTIONSand makeresolveIntervalMs(interval, customMs)(line 31) return an explicit no-interval sentinel (null) for it. Today theswitchends indefault: return DAY, so an unrecognized or on-demand value silently becomes a daily job. That fall-through is the core bug this issue must close.Every
resolveIntervalMsconsumer must handle the sentinel explicitly — arithmetic onnullyieldsNaN/Invalid Date:server/services/autonomousJobs/store.js:190(migrateScriptsState)server/services/autonomousJobs/crud.js:95(createJob) and:169(updateJob)client/src/utils/cronHelpers.js—JOB_INTERVAL_OPTIONS(line 287) is the hand-maintained client mirror of the server list and must gain the same rowDue / next-due — there are three separate implementations, all of which need the sentinel:
server/services/autonomousJobs/scheduler.js#getDueJobs(line 36) and#computeNextJobRun(line 108)server/services/cosJobScheduler.js#computeNextJobFireTime(line 52) — this is the one that actually arms the timer viaregisterSingleJobSchedule/registerJobSchedules, notscheduler.js. An on-demand job must register no timer while stayingenabled.Validation —
createCosJobSchema(server/lib/cosValidation.js:406) declaresinterval: z.string().optional(), a bare free-form string, so a typo'd cadence already reaches disk and silently reschedules daily. Tighten it to the cadence enum (including the new value) so the boundary rejects it;updateCosJobSchema(:464) inherits via.partial().intervalMsmust not be required or back-filled for the on-demand cadence.UI —
client/src/components/apps/tabs/CustomTasksSection.jsxowns the form state andtoPayload, but the cadence widgets areScheduleFields, exported fromclient/src/components/cos/JobCard.jsx(line 91; the<select>bound todata.intervalis at 133, cadence label at 376, due computation at 160 and 330). Offer the cadence, hide/disable the interval-only inputs when it is selected, and render next-run as "On demand" rather than a date.GET /api/cos/jobs/intervals(server/routes/cosJobRoutes.js) servesINTERVAL_OPTIONSverbatim, so the new row flows through automatically.Preserve existing recurring cadences and
job:spawnedbookkeeping exactly. Do not reintroduce a per-appquota-burnscheduled task type.Acceptance criteria
resolveIntervalMsreturns the explicit no-interval sentinel for the on-demand cadence and never falls through toDAY; a test asserts the sentinel is neitherDAYnorNaN.cosJobScheduler.registerSingleJobSchedulearms no timer for an on-demand job, and next-due surfaces render noNaN/Invalid Date.createCosJobSchema/updateCosJobSchemareject an unknown cadence string (closing the existing silent-daily typo hole) and accept the new value withoutintervalMs.POST /api/cos/jobs/:id/triggerwith unchanged approval/lifecycle rules.intervalMs, and next-due values — a round-trip test over everyINTERVAL_OPTIONSvalue pluscustom.INTERVAL_OPTIONSandJOB_INTERVAL_OPTIONSstay in lockstep, with a test that fails when they drift.ScheduleFields, the hidden interval inputs, and the "On demand" next-run label.Notes
Independent of the rest of #6372 and safe to ship first. Children that reference custom tasks (the migration and the picker UI) depend on this cadence existing.
Part of #6372
When this ships, tick its box in the
## Decomposed intochecklist on #6372. If it is the last unchecked box, close #6372.