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:
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:
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.
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:
For example, attempting to represent the first Thursday using a standard expression such as:
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:
Proposed solution
Add a Calendar schedule type alongside the existing Cron and Run once options.
The calendar schedule UI could provide structured fields:
Example:
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
-1forordinalcould represent the final occurrence in the month.Validation and safety
Before saving, display the next several calculated execution dates. For example:
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:
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