Skip to content

Feature: Add calendar-based scheduling for ordinal weekdays #4191

Description

@samwozencroft

Related to

Web frontend (schedule configuration), Service (scheduled task execution)

Related discussion: #2349
Related examples: #3600, #2241, #2087

Impact

Safer and more expressive task scheduling.

Problem

Semaphore currently relies on basic cron expressions. This makes common maintenance schedules difficult or impossible to express safely, including:

  • First Thursday of every month
  • Third Tuesday of every month
  • Last Wednesday of every month
  • Last day of every month

For example, attempting to represent the first Thursday using a standard expression such as:

0 9 1-7 * 4

does not mean "Thursday occurring between the 1st and 7th" under traditional cron semantics. When both day-of-month and day-of-week are restricted, they are commonly evaluated using OR logic. The task can therefore run on every Thursday and on every date from the 1st through the 7th.

This is easy to misunderstand because the expression appears to describe the intended intersection. It can result in unexpected task execution, as happened in our environment.

The current workaround is to schedule the task weekly and add date checks inside the Ansible playbook. That:

  • Creates task runs which intentionally do nothing
  • Clutters execution history and alerts
  • Couples scheduling rules to playbook logic
  • Makes schedules harder to audit from Semaphore
  • Must be repeated across playbooks

Proposed solution

Add a Calendar schedule type alongside the existing Cron and Run once options.

The calendar schedule UI could provide structured fields:

  • Frequency: monthly
  • Ordinal: first, second, third, fourth or last
  • Weekday: Monday through Sunday
  • Time
  • Timezone

Example:

Monthly → First → Thursday → 09:00 → Europe/London

This should be stored as structured schedule data rather than converted to an ambiguous five-field cron expression.

On the service side, calculate and register the next matching date. After execution, calculate the following occurrence and register it again. Existing cron schedules would continue using the current implementation unchanged.

A possible stored representation:

{
  "type": "calendar",
  "frequency": "monthly",
  "ordinal": 1,
  "weekday": 4,
  "time": "09:00",
  "timezone": "Europe/London"
}

Using -1 for ordinal could represent the final occurrence in the month.

Validation and safety

Before saving, display the next several calculated execution dates. For example:

3 September 2026 at 09:00
1 October 2026 at 09:00
5 November 2026 at 09:00

This preview would expose incorrect scheduling assumptions before a production task is enabled.

The backend should use the same calculation for both the preview and actual execution so the UI cannot disagree with the scheduler.

Alternative implementation

If a structured calendar schedule is considered too large a change, Semaphore could add support for an established extended expression format such as Quartz-style ordinal weekdays:

0 0 9 ? * THU#1

However, a structured calendar mode would be clearer for users, easier to validate, and would preserve the existing cron behaviour without changing the interpretation of current schedules.

Acceptance criteria

  • A user can schedule a task for the first Thursday of every month without playbook-level date checks.
  • First through fourth and last weekday occurrences are supported.
  • The schedule has an explicit timezone.
  • The UI displays at least the next three execution dates before saving.
  • Previewed dates and actual execution dates are calculated by the same backend implementation.
  • Existing cron schedules retain their current behaviour.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions