cronjob implementation
UI invalidation
Maintainer note: creating, saving or deleting a job refreshes the jobs list, calendar, run history and host conversation links through the plugin's useInvalidateJobs hook. Run now from a job drawer, calendar card or failed-run result uses the same hook after the server accepts the request. The drawer owns its refresh, including an already-queued response; its callers do not repeat it. Calendar state labels and the History and result badges share web-src/runStatus.ts; calendar dots keep their own CSS tones. Detailed run receipts are retained for seven days, daily aggregates for ninety days, and result previews for up to 2,000 characters. These journal limits are fixed; the journal's optional clock is used by tests.
Browser settings contract
The job rail and creation dialogs remain available from both the Automation page and the plugin settings
deck. They share ScheduleField, ActiveHoursField and the existing conversation/destination adapters.
Simple schedule, interval-unit, active-hours, owner, preset and suggested-time choices use shared dropdowns;
the calendar's day/week navigation remains segmented. Presets fill an editable draft rather than binding a
job to a template, and custom times remain visible. For one-shot creation, daily and weekly presets may
fill a blank time through the existing schedule parser; interval presets leave the time for the user to choose.
Forms use the host's SettingsDocument, SettingsGroup and SettingsRow. Schedule and Advanced groups
start closed and persist through the host's folding mechanism. Saved-job keys are
cronjob:job:<id>:schedule and cronjob:job:<id>:advanced; creation keys are
cronjob:create:<lifecycle>:schedule and cronjob:create:<lifecycle>:advanced. Both entry points share
the same saved-job keys. Untitled leading fields and read-only run information do not become disclosures.
Groups stay mounted when closed, so folding does not reset a draft or change autosave.
Browser UI API 51 is required for Input.unit. Interval amounts display min or h, and active-hour
clock values display h, using the host's shared numeric band. Schema millisecond limits display ms
and the shortest interval displays min, without a divisor or storage conversion. Count meaning belongs
in the localized label. Native input names, bounds and steps remain intact. Advanced cron validation stays
on the server; unrepresented schedules and stored active-hours expressions remain unchanged until edited.
Job edits retain revision-checked debounced autosave; creation retains explicit idempotent submission.
See the Scheduling Plugin page in the Elowen user manual.
Server module ownership
index.mjs composes the existing registrations in their original order. It opens the three atomic JSON
stores and the SQL run journal, registers account cleanup/reconciliation, connects the concrete readers
and writers below, then registers the platform adapter and shipped skills.
lib/scheduler.mjsownsCronAdapter, guard execution, due/manual claims, relay, owner alerts and leased pending deliveries. ItsjobRunLocationalso supplies calendar/navigation/retention reads. The host still injects the live timezone and owner-permission readers; these required inputs have no constructor defaults. The adapter keeps one sequential tick per generation and stops future work at teardown.lib/stores.mjsowns jobs, creation receipts and pending-delivery persistence, strict write reads, model-pair validation and the draft/creation rules shared by HTTP and tools. Writes remain synchronous atomic JSON operations; the existing row-before-receipt crash window and delivery leases are unchanged.lib/runJournal.mjsremains the SQL authority for claims, run receipts and retention.lib/access.mjsowns account grants, owner limits, explicit tool scope and the actor visibility rule. Each execution rechecks permissions. Missing permission reads retain their existing fail-closed behavior.lib/filing.mjsowns immutable conversation associations, metadata-only conversation discovery and the unchangedcroncontrol methods,retainedSessionIdsandconversationLinks. Filing never changes execution or delivery. Missing and unavailable associations remain distinct.lib/routes.mjsregisters the existing jobs GET/PUT/POST/DELETE routes. Authorization still precedes conflict payloads, and the final strict read follows awaited project authorization before a synchronous save.lib/tools.mjsregisters the same five tools through the shared access, filing and write readers.lib/calendar.mjsowns job-response enrichment, the live engine-input reader and week/history/preview routes. Timezone, generation time, tick and lookback are read at request time; intentional preview overrides remain at their callers. Empty results and unreadable jobs still have separate responses.schedule.mjsowns parsing, due/slot decisions, wall-clock resolution, labels and schedule defaults.projection.mjsowns bounded forward expansion and arithmetic day summaries, importing that same parser, cron-day predicate, DST identity and active-hours gate. Projection is pure and performs no storage or network work; its existing candidate and day-slot caps remain unchanged.lib/constants.mjsholds only the shared id generator and JSON response constructor.
For example, a job-list GET filters with the access reader, enriches filing and next-occurrence data through the calendar reader, and projects from the same schedule authority used by the next scheduler tick. No additional host seam or public control/route contract is introduced.
Shared time-zone helpers
The server schedule engine and the scheduler import zonedParts, zonedTimeToMs and WEEKDAYS from
elowen-plugin-shared/zonedTime. The shared module owns the formatter cache, local wall-clock fields,
UTC conversion and Sunday-first weekday order. No local copies or re-exports remain in schedule.mjs.
The cron parser, slot identity, due logic and previews stay in this plugin and use those helpers.
This keeps the same time-zone and DST behavior while letting meeting-confirm use the same conversion.
The helpers do no network or storage work. An invalid zone retains the existing machine-zone behavior; schedules, catch-up and duplicate DST slots are unchanged. The plugin requires the core release shipping the helper; shared API version 8 is unchanged. The minimum core version is declared in the current manifest and registry. An older core cannot load this plugin.
Developer reference
History retention and daily totals
GET /plugins/cronjob/api/runs?view=daily&from=2026-09-01&to=2026-09-30 returns { dailySummaries, total }. Each summary contains jobId, jobName, captured ownerUserId, localDate, timezone, ok, error, skipped and total. It uses the same actor visibility and owner/date/job/search/outcome filters as run history, but searches only retained job names and uses limit/offset pagination, capped at 100 rows. An outcome filter selects summaries containing that outcome; all counters remain the day's totals. Without view=daily the existing detailed run list and cursor contract remain. Empty retained history returns an empty summary list. HistoryTab requests both views with independent pagers and never offers run, transcript or result actions for summary rows. Detailed terminal runs older than seven days become summaries retained for 90 days. countsByDay adds both stores without counting pruned details twice.
Captured aggregate ownership
Plugin migration 4 preserves every existing daily-row field and replaces uniqueness by job, local date and captured owner, treating NULL as the instance bucket. Pruning groups by the captured owner and never overwrites another owner's counters on a transfer. Migration applies through the host plugin DB migrator at load; it never needs a live-DB maintenance script. Historical rows retain the ownership recorded by schema 3; prior overwritten attribution cannot be reconstructed from aggregates. HistoryTab is the reader.
Shared live schedule inputs and preview refusal
createJobProjection reads timezone and generation time plus the active CronAdapter's tickMs and cronLookbackMs. Historical week cards override the reference time to just before their local day; draft previews otherwise use the same live policy as the scheduler. POST schedule-preview retains count and fromLocalDate. If planOccurrences exhausts its 100000-candidate budget, the route returns HTTP 422, code preview_budget_exhausted and no occurrences. Invalid grammar remains valid:false with its existing response contract. The advanced editor displays the host API error; valid ordinary modes keep their existing controls. No browser cron expansion is introduced.
Scoped command execution
Cronjob's projectCheck calls the live Sandbox control's runCommand with leaseKind cron, the existing projectRef and explicit account/root actor, the guard timeout/signal and a combined 1 MiB output limit. Core owns capture, cancellation, heartbeat, sanitization and lease release. A missing Sandbox control fails the guard; nonzero or signal exits remain failed checks and producer errors, including cleanup failures, remain errors. Host target and owning-account access are checked at authorization and again before the guard. The scheduler is the consumer.
Shared filing, local times and grammar
CronAdd and HTTP recurring creation use createJobWrites.buildRecurringJob and the existing associationEdit filing authority. A tool supplies its current project binding explicitly even for an instance job; HTTP instance creation remains unbound. Personal creation preserves its existing binding. Direct-delivery origins are supplied only by the tool path and remain independent of filing. createJobWrites.localRunAt is the one create/edit mapper for local one-shots, preserving spring-gap errors and earlier/later instants. ACTIVE_HOURS_PATTERN in scheduleGrammar is shared by daemon active-hour validation and the browser builder; historical malformed-hour handling is unchanged.
Public response cuts
Run-history rows no longer expose requestId or initiatorUserId; operation views and persisted audit/dedupe columns keep them. Week responses omit generatedAt, nowLocalTime, precisionMs and truncated; scheduler exposes only ready. Interval rows retain jobId, schedule, enabled, nextLocalDate and nextLocalTime, removing nextExpectedAt, remainingToday, lastOutcome and lastRunAt. Detailed nextOccurrence keeps expectedAt. planOccurrences returns occurrences/truncated while candidate counting stays private. summarizeJobDay returns exact remaining/head without kind/truncated, bounded to at most 1440 fixed wall clocks and a short head. resolveLocalDateTime returns ms or its existing error; later disambiguation remains. The unused isDue wrapper is removed; dueSlot is the scheduler authority.
Host browser and operation seams
Cronjob uses ElowenUiRuntime.api directly with the published JSON option for create, save, run and preview requests, and its published utils.interpolate type for all existing interpolated consumers. No local serializer or fallback is added. Modal callers no longer pass intent. useRunCronJob returns Promise