Skip to content

feat(schedules): add monthly weekday schedules (Nth weekday and offset) - #4116

Open
makaiver wants to merge 7 commits into
semaphoreui:developfrom
makaiver:feat/schedule-monthly-weekday
Open

feat(schedules): add monthly weekday schedules (Nth weekday and offset)#4116
makaiver wants to merge 7 commits into
semaphoreui:developfrom
makaiver:feat/schedule-monthly-weekday

Conversation

@makaiver

@makaiver makaiver commented Aug 2, 2026

Copy link
Copy Markdown

A suggestion on how to add support for scheduling tasks like "the third Wednesday of the month" or "the first Wednesday after Patch Tuesday," which standard cron can't express.

You write it as a simple schedule descriptor in the existing cron field:

@monthly-weekday [offset ] [at HH:MM]

Examples:

  • @monthly-weekday 3 wed at 09:00 → third Wednesday, 9am
  • @monthly-weekday 2 tue offset 1 at 03:00 → first Wednesday after Patch Tuesday
  • @monthly-weekday last fri at 22:30 → last Friday

There's also a "Monthly (by weekday)" option in the schedule form - its Hours/Minutes use the same chip grids as the existing Monthly builder, plus Occurrence, Weekday, and an Offset (days) field - and the "Next run time" preview works for it. The offset makes patch-Tuesday-relative windows easy (Second Tuesday + 1 = the Wednesday after Patch Tuesday), which is handy when patching Windows servers.

Addresses #3600, #2349, #2241, #1172 - all requests for this (first Tuesday, third Friday, last Wednesday, first Monday of the month), which people have been working around with uggly hacks and external cron.

Notes

  • Reuses the existing cron_format field - no DB migrations needed.
  • No new dependency so it should work in HA mode.

Summary by CodeRabbit

  • New Features
    • Added monthly weekday scheduling with ordinal or last-weekday options, offsets, custom times, and timezone support.
    • Added schedule form controls and upcoming-run previews.
    • Added validation with the next five activation times for valid schedules.
    • Added support for standard cron expressions, descriptors, and timezone prefixes.
  • Bug Fixes
    • Improved handling and error messages for invalid scheduling formats.
    • Preserved existing cron behavior while supporting the new scheduling options.

- Add "@monthly-weekday <ordinal> <weekday> [offset <days>] [at <HH:MM>]"
  cron descriptor, e.g. first Wednesday after Patch Tuesday
- Custom cron.ScheduleParser backs both the pool and ValidateCronFormat and
  delegates every other expression to the standard parser
- Reuse cron_format (no migration); HA-safe; no new dependency
- Return next_run from the validate endpoint for the UI preview
- Add "Monthly (by weekday)" builder to ScheduleForm, matching the Monthly design
@coderabbitai

coderabbitai Bot commented Aug 2, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Added monthly-weekday cron descriptors with ordinal, weekday, offset, time, and timezone support. Added backend validation previews, frontend scheduling controls, parser integration, and behavior and compatibility tests.

Changes

Monthly weekday scheduling

Layer / File(s) Summary
Schedule parser and execution
services/schedules/monthly_weekday.go, services/schedules/SchedulePool.go, AGENTS/plans/...
The scheduler parses monthly-weekday descriptors, calculates occurrences with offsets and timezone handling, generates previews, and uses the custom parser for execution and validation.
Schedule validation preview API
api-docs.yml, api/projects/schedules.go, api/projects/schedules_test.go
The API documents supported cron formats and returns up to five upcoming activation times or validation errors.
Monthly-weekday schedule form
web/src/components/ScheduleForm.vue
The form adds monthly-weekday controls, builds and parses descriptors, and uses backend previews when client parsing cannot evaluate them.
Schedule behavior and compatibility validation
services/schedules/monthly_weekday_test.go, services/schedules/monthly_weekday_property_test.go, services/schedules/monthly_weekday_regression_test.go
Tests cover occurrence calculation, offsets, DST, parser delegation, validation safety, pool execution, preview parity, broad input cases, and regressions.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant ScheduleForm
  participant ValidateScheduleCronFormat
  participant NextRunTimes
  participant newCronParser
  ScheduleForm->>ValidateScheduleCronFormat: submit monthly-weekday cron format
  ValidateScheduleCronFormat->>NextRunTimes: request five upcoming runs
  NextRunTimes->>newCronParser: parse schedule descriptor
  newCronParser-->>NextRunTimes: parsed schedule
  NextRunTimes-->>ValidateScheduleCronFormat: next_run timestamps
  ValidateScheduleCronFormat-->>ScheduleForm: validation response
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 64.81% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: adding monthly weekday schedules with ordinal weekdays and offsets.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
web/src/components/ScheduleForm.vue (1)

720-740: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Discard stale validation responses before writing backendNextRuns.

refreshCron() calls validateCronFormat() without awaiting it, and the offset field triggers one call per keystroke. Two in-flight requests can resolve out of order. The later-resolving older response then overwrites backendNextRuns, and the "Next run time" preview shows runs for a descriptor the user already replaced. The preview does not self-correct until the next edit.

refreshCheckboxes() already guards against this at Line 825. Apply the same guard inside validateCronFormat() so every caller benefits.

🐛 Proposed staleness guard
     async validateCronFormat(cronFormat) {
       try {
         const res = await axios({
           method: 'post',
           url: `/api/project/${this.projectId}/schedules/validate`,
           responseType: 'json',
           data: {
             project_id: this.projectId,
             cron_format: cronFormat,
           },
         });
+        if (cronFormat !== this.item.cron_format) {
+          return null; // the value changed while validating, ignore stale result
+        }
         const runs = (res.data && res.data.next_run) || [];
         this.backendNextRuns = runs
           .map((s) => new Date(s))
           .filter((d) => !Number.isNaN(d.getTime()));
         return null;
       } catch (err) {
+        if (cronFormat !== this.item.cron_format) {
+          return null;
+        }
         this.backendNextRuns = [];
         return getErrorMessage(err);
       }
     },
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web/src/components/ScheduleForm.vue` around lines 720 - 740, Update
validateCronFormat to use the same request-generation or staleness guard pattern
as refreshCheckboxes before assigning backendNextRuns, including the error path
that clears it. Ensure only the latest validation request may update
backendNextRuns, while preserving the existing successful parsing and
error-message behavior.
🧹 Nitpick comments (2)
services/schedules/monthly_weekday_regression_test.go (1)

38-81: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Add a short-mode escape to the new exhaustive sweeps in services/schedules. Three new tests in this package run large generated sweeps with no testing.Short() guard, so every CI run pays the full cost. The shared fix is one skip guard per sweep.

  • services/schedules/monthly_weekday_regression_test.go#L38-L81: add if testing.Short() { t.Skip(...) } at the start of TestDiffSweep_NoRegressionVsParseStandard, which generates 23,520 specs and parses each one twice.
  • services/schedules/monthly_weekday_property_test.go#L110-L141: add the same guard to TestMonthlyWeekday_Prop_BruteForceOracle, which compares 378 schedules against 12 reference instants with a day-by-day oracle.
  • services/schedules/monthly_weekday_property_test.go#L146-L167: add the same guard to TestMonthlyWeekday_Prop_OneMonthBackSufficiency, which steps every 6 hours across two years for each of 5 schedules.

Keep the full sweeps for the normal go test run.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@services/schedules/monthly_weekday_regression_test.go` around lines 38 - 81,
Add an early testing.Short() skip guard to
TestDiffSweep_NoRegressionVsParseStandard in
services/schedules/monthly_weekday_regression_test.go:38-81,
TestMonthlyWeekday_Prop_BruteForceOracle in
services/schedules/monthly_weekday_property_test.go:110-141, and
TestMonthlyWeekday_Prop_OneMonthBackSufficiency in
services/schedules/monthly_weekday_property_test.go:146-167. Keep the complete
generated sweeps running during normal go test execution.
web/src/components/ScheduleForm.vue (1)

234-244: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Bound the offset input and debounce the validation request.

The @input handler calls refreshCron() on every keystroke, and refreshCron() posts to /schedules/validate for the monthly-weekday timing. Typing a two-character offset such as -5 sends two requests, and one of them is for the intermediate value -. Add min="-28" and max="28" to match the backend limit of ±28 days, and debounce the handler or switch to @change.

♻️ Proposed change to the offset field
             <v-text-field
               v-model.number="mwOffset"
               type="number"
+              min="-28"
+              max="28"
               label="Offset (days)"
               hint="e.g. Second Tuesday + 1 = first Wednesday after Patch Tuesday"
               persistent-hint
               :disabled="formSaving"
-              `@input`="refreshCron()"
+              `@change`="refreshCron()"
               outlined
               dense
             />
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web/src/components/ScheduleForm.vue` around lines 234 - 244, Update the
mwOffset v-text-field to enforce the backend range with min="-28" and max="28",
and stop calling refreshCron() for every keystroke by debouncing the handler or
switching it to `@change` so intermediate values such as "-" do not trigger
validation requests.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@services/schedules/monthly_weekday_property_test.go`:
- Around line 43-46: Update the doc comment for oracleNext to describe the
actual enumeration window implemented by its month-2 start and 12-month
iteration: t-2 months through t+9 months. Leave the code and the inline comment
unchanged.

In `@web/src/components/ScheduleForm.vue`:
- Around line 952-959: Update refreshCron() in the monthlyWeekday branch to
assign the return value of validateCronFormat(this.item.cron_format) to
this.cronFormatError, matching the existing handling in refreshCheckboxes().
Preserve the backend-authoritative preview refresh and early return behavior.

---

Outside diff comments:
In `@web/src/components/ScheduleForm.vue`:
- Around line 720-740: Update validateCronFormat to use the same
request-generation or staleness guard pattern as refreshCheckboxes before
assigning backendNextRuns, including the error path that clears it. Ensure only
the latest validation request may update backendNextRuns, while preserving the
existing successful parsing and error-message behavior.

---

Nitpick comments:
In `@services/schedules/monthly_weekday_regression_test.go`:
- Around line 38-81: Add an early testing.Short() skip guard to
TestDiffSweep_NoRegressionVsParseStandard in
services/schedules/monthly_weekday_regression_test.go:38-81,
TestMonthlyWeekday_Prop_BruteForceOracle in
services/schedules/monthly_weekday_property_test.go:110-141, and
TestMonthlyWeekday_Prop_OneMonthBackSufficiency in
services/schedules/monthly_weekday_property_test.go:146-167. Keep the complete
generated sweeps running during normal go test execution.

In `@web/src/components/ScheduleForm.vue`:
- Around line 234-244: Update the mwOffset v-text-field to enforce the backend
range with min="-28" and max="28", and stop calling refreshCron() for every
keystroke by debouncing the handler or switching it to `@change` so intermediate
values such as "-" do not trigger validation requests.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 46a630cd-c6e6-4d17-976b-6d8b27ca0e7b

📥 Commits

Reviewing files that changed from the base of the PR and between ff0cf4c and 0c1f39a.

📒 Files selected for processing (10)
  • AGENTS/plans/2_20/schedule-monthly-weekday.md
  • api-docs.yml
  • api/projects/schedules.go
  • api/projects/schedules_test.go
  • services/schedules/SchedulePool.go
  • services/schedules/monthly_weekday.go
  • services/schedules/monthly_weekday_property_test.go
  • services/schedules/monthly_weekday_regression_test.go
  • services/schedules/monthly_weekday_test.go
  • web/src/components/ScheduleForm.vue

Comment thread services/schedules/monthly_weekday_property_test.go
Comment thread web/src/components/ScheduleForm.vue Outdated
…reviews

- Guard validateCronFormat() against out-of-order responses: a stale
  reply no longer overwrites backendNextRuns or the error state
- refreshCron() now surfaces a backend rejection (reachable through the
  free-input offset field) instead of silently blanking the preview;
  shown on the Offset field, since the raw-cron field is hidden in
  builder mode
- Correct the oracle doc comment window (t-2 .. t+9 months)
Guard the three generated sweeps (differential vs ParseStandard, the
brute-force oracle, and the two-year sufficiency check) with
testing.Short() so `go test -short` skips them.
- Add min=-28 / max=28 to the "Monthly (by weekday)" offset field to
  match the backend ±28-day limit
- Switch @input to @change so validation fires once per committed value
  instead of on every keystroke (no request for the intermediate "-")
…lities

Add doc comments to the two remaining parser helpers (parseOffset,
parseHourMinute), the validate handler, and the test helpers whose
purpose isn't obvious from the name (safeParse, descOf, fmtT,
mustParseAmbient, setupScheduleConfig, postValidate) plus the two
property tests with cryptic names. Self-describing test functions are
left uncommented, matching the package style.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds first-class support for “Nth weekday of the month” scheduling (including an optional day offset) by introducing a new @monthly-weekday cron descriptor, wiring it into the backend scheduler/validator, and adding a corresponding UI builder + next-run preview support.

Changes:

  • Backend: add a custom cron parser and schedule implementation for @monthly-weekday …, and use it for both scheduling and validation.
  • API/UI: extend the schedules validate endpoint to return upcoming run times and use that to power the “Next run time” preview for descriptors the frontend cron parser can’t evaluate.
  • Frontend: add a “Monthly (by weekday)” schedule builder that emits the descriptor into the existing cron_format field.

Reviewed changes

Copilot reviewed 10 out of 10 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
web/src/components/ScheduleForm.vue Adds “Monthly (by weekday)” builder, descriptor parsing/formatting, and backend-powered preview fallback.
services/schedules/SchedulePool.go Switches the cron pool and validation to a shared custom parser to keep “validates == fires”.
services/schedules/monthly_weekday.go Implements @monthly-weekday parsing, schedule evaluation, and preview (NextRunTimes).
services/schedules/monthly_weekday_test.go Unit tests for core monthly-weekday behavior, errors, timezone behavior, and preview.
services/schedules/monthly_weekday_regression_test.go Differential tests to ensure no regressions vs cron.ParseStandard.
services/schedules/monthly_weekday_property_test.go Property/oracle tests for correctness, DST behavior, and end-to-end pool parity.
api/projects/schedules.go Extends /schedules/validate to return next_run on success.
api/projects/schedules_test.go Tests validate endpoint behavior for descriptor, standard cron, and invalid inputs.
api-docs.yml Documents the new descriptor and the validate endpoint response shape.
AGENTS/plans/2_20/schedule-monthly-weekday.md Captures the implementation plan/design notes for the feature.

Comment thread web/src/components/ScheduleForm.vue Outdated
makaiver and others added 2 commits August 3, 2026 10:19
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
The applied validation guards dropped the function's closing "}," which
broke parsing, and left the hour/minute check over the line-length limit.
Add the brace back and split that check into two lines.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants