Modified from https://github.com/Raemi/org-reschedule-by-rule.
Key Differences:
- Uses a cron parser implemented in pure Elisp, with no dependency on the Python croniter package.
- Replaces the
INTERVALproperty with aDAY_ANDproperty. - Supports toggling between SCHEDULED and DEADLINE timestamps.
org-repeat-by-cron.el is a lightweight extension for Emacs Org
mode that allows you to repeat tasks based on powerful Cron expressions.
Standard Org mode repeaters (like +1d , ++1w ) are based on the current SCHEDULED or DEADLINE timestamp. In contrast, this tool provides a repetition method based on absolute time rules. You can easily set a task to repeat “on the last Friday of every month” or “on the first Monday of each quarter” without manual date calculations.
A core advantage of this tool is its pure Elisp implementation, which does not rely on any external programs (like Python’s croniter library), ensuring it works out-of-the-box in any Emacs environment.
- Define complex repetition rules using standard 5-field Cron expressions (minute, hour, day, month, day of week).
- Pure Elisp implementation with no external dependencies for simple installation and efficient operation.
- Supports extended Cron syntax:
L: Represents the “last day” of the month.W: Represents the nearest “weekday”.#: Represents the “Nth day-of-week X”, e.g.,5#2means the second Friday.
- Supports toggling the logic between the “day of month” and “day of week” fields using the
DAY_ANDproperty (default isOR). - Supports English abbreviations for months (
JAN-DEC) and days of the week (SUN-SAT) for improved readability. - You can choose to update the task’s
SCHEDULEDtimestamp orDEADLINEtimestamp (defaults toSCHEDULED).
Install via Melpa or by manually downloading the org-repeat-by-cron.el file.
The recommended configuration is as follows:
(use-package org-repeat-by-cron
:ensure nil ; If the file is already in your load-path
:config
(global-org-repeat-by-cron-mode))To make an Org task repeat according to a Cron rule, simply add the REPEAT_CRON property to its PROPERTIES drawer.
REPEAT_CRON: ( Required ) A string containing the Cron expression.- 5-field format: minute hour day-of-month month day-of-week (e.g.,
0 9 * * *for every day at 9:00 AM). - 3-field format: day-of-month month day-of-week (minute and hour default to
0 0).
- 5-field format: minute hour day-of-month month day-of-week (e.g.,
REPEAT_DAY_AND: (Optional) If set to t, the “day of month” and “day of week” fields must both be satisfied (ANDlogic). If not set or set to any other value, only one of them needs to be satisfied (ORlogic, which is standard Cron behavior).REPEAT_DEADLINE: (Optional) If set to t, the task’sDEADLINEtimestamp will be updated when completed. Otherwise, theSCHEDULEDtimestamp is updated by default.
- Add the
REPEAT_CRONproperty and set your Cron expression under an Org heading. - (Optional) Add
REPEAT_DAY_ANDorREPEAT_DEADLINEas needed. - Change the task’s
TODOstate toDONEas you normally would. - What happens automatically:
org-repeat-by-crondetects the state change.- It calculates the next valid time based on the
REPEAT_CRONrule. - It updates the task’s
SCHEDULEDorDEADLINEtimestamp. - It resets the task’s
TODOstate fromDONEback toTODO(or your default initial state).
Tip: You should not use org-repeat-by-cron and the built-in Org repeater cookie (e.g., +1w) on the same task.
A Cron expression consists of 5 fields, separated by spaces.
| Field | Allowed Values | Allowed Special Characters |
|---|---|---|
| Minute | 0-59 | \* , - / |
| Hour | 0-23 | \* , - / |
| Day of Month | 1-31 | \* , - / ? L W |
| Month | 1-12 or JAN-DEC | \* , - / |
| Day of Week | 0-7 (0 and 7 are both Sunday) or SUN-SAT | \* , - / ? L # |
| Character | Description | Example |
| \* | Matches any value in the field. | \* in the “hour” field means “every hour”. |
| , | Separates multiple values. | 1,15 in the “day” field means “on the 1st and 15th of the month”. |
| - | Defines a range. | MON-FRI in the “day of week” field means “from Monday to Friday”. |
| \/ | Defines a step value. | \*/15 in the “minute” field means “every 15 minutes”. |
L- “Last”. In the “day of month” field, L means the last day of the month. In the “day of week” field, 5L means the last Friday of the month.
L(day) -> Jan 31st,L6(day of week) -> the last Saturday of the month.
W- “Weekday” (Monday-Friday) nearest the given day.
15Wwill find the nearest weekday to the 15th. If the 15th is a Saturday, it matches the 14th (Friday); if the 15th is a Sunday, it matches the 16th (Monday). It does not cross months: if the 1st is a Saturday,1Wwill match the 3rd (Monday); if the 31st is a Sunday,31Wwill match the 29th (Friday).
- “Weekday” (Monday-Friday) nearest the given day.
LW- “Last weekday of the month” .
#- “The Nth day of week in the month”. Format:
DOW#N. 5#2-> The second Friday.1#1,1#3-> The 1st and 3rd Monday.
- “The Nth day of week in the month”. Format:
A task that needs to repeat every Friday at 5:00 PM.
\* TODO Submit weekly report
SCHEDULED: <2025-09-12 Fri 17:00>
:PROPERTIES:
:REPEAT_CRON: "0 17 * * FRI"
:END:
A reminder to pay a bill on the last day of every month (using the 3-field format).
\* TODO Pay credit card bill
SCHEDULED: <2025-09-30 Tue>
:PROPERTIES:
:REPEAT_CRON: "L * *"
:END:
A meeting that occurs only on the first and third Monday of each month.
\* TODO Attend bi-weekly tech sync meeting
DEADLINE: <2025-10-06 Mon 10:00>
:PROPERTIES:
:REPEAT_CRON: "0 10 * * MON#1,MON#3"
:REPEAT_DEADLINE: t
:END:
A task to be performed on the first Monday of the first month of each quarter.
\* TODO Perform quarterly server maintenance
SCHEDULED: <2025-10-06 Mon>
:PROPERTIES:
:REPEAT_CRON: "1 1 * JAN,APR,JUL,OCT MON#1"
:END: