An Org-mode task repeater based on Cron expressions.
Modified from org-reschedule-by-rule.
- Pure Elisp Parser: Implemented in pure Elisp with no dependency on external Python packages like
croniter. - Logic Control: Replaces the
INTERVALproperty withDAY_ANDfor sophisticated date matching (AND/OR logic for DOM and DOW). - Dual Timestamp Support : Supports independent rescheduling for both
SCHEDULEDandDEADLINEtimestamps simultaneously. - Maximum Compatibility: Specifically designed to work with
org-habit. It allows Org-mode to handle the repeat process natively, then automatically recalculating and setting the precise date based on Cron rules.
org-repeat-by-cron.el is a lightweight extension for Emacs Org-mode that allows you to repeat tasks using the power of Cron expressions.
Standard Org-mode repeaters (like +1d, ++1w) are relative to the current SCHEDULED or DEADLINE timestamp. In contrast, this tool provides repetition 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 is its pure Elisp implementation, ensuring it works out-of-the-box in any Emacs environment without external dependencies.
- Define complex repetition rules using standard 5-field Cron expressions (
Minute, Hour, Day, Month, Day of Week). - Zero external dependencies; easy to install and efficient to run.
- Supports advanced Cron extensions:
L: Represents the “Last” day of the month or the last specific weekday of the month.W: Represents the “Nearest Weekday” (Mon-Fri) to a specific date.LW: “Last Weekday” of the month.#: Represents the “Nth weekday of the month” (e.g.,5#2for the second Friday).
- Toggle logic for “Day of Month” and “Day of Week” fields using the
DAY_ANDproperty (defaults toOR). - Supports English aliases for months (
JAN-DEC) and days (SUN-SAT) for better readability. - You can choose to let it update the task’s
SCHEDULEDtimestamp orDEADLINEtimestamp, or update both independently (default isSCHEDULED).
Available via MELPA.
Recommended configuration:
(use-package org-repeat-by-cron
:ensure t
: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.
Tip: You do not need to wrap the REPEAT_CRON value in quotes.
Suppose we have a weekly course held on weekends:
* TODO Weekend Course
:PROPERTIES:
:REPEAT_CRON: * * SAT,SUN
:END:
When marked as DONE, it will automatically be rescheduled to the next matching date. For example, if today is Dec 9, 2025, it will be scheduled for the coming Saturday (Dec 13, 2025):
* TODO Weekend Course
SCHEDULED: <2025-12-13 Sat>
:PROPERTIES:
:REPEAT_CRON: * * SAT,SUN
:REPEAT_ANCHOR: 2025-12-13 Sat
:END:
Marking it DONE again will calculate the next point based on the REPEAT_ANCHOR and the current time:
* TODO Weekend Course
SCHEDULED: <2025-12-14 Sun>
:PROPERTIES:
:REPEAT_CRON: * * SAT,SUN
:REPEAT_ANCHOR: 2025-12-14 Sun
:END:
Note: If you do not want to specify a specific hour/minute, use the 3-field shorthand (discussed below).
REPEAT_CRON: (Required) A string containing the Cron expression.- 5-field format:
Min Hour Dom Month Dow(e.g.,0 9 * * *for daily at 09:00). Generates a timestamp withHH:MM. - 3-field format:
Dom Month Dow. Generates a date-only timestamp withoutHH:MM.- Why use 3 fields? In Org-mode, many tasks only need a date. If you want the task to appear in the “All Day” section of your Agenda, use the 3-field format. Use 5 fields only if you need a specific start time.
- 5-field format:
REPEAT_DAY_AND: (Optional) Defaults to nil (logicOR). Set totto enableANDlogic.- OR (Default):
13 * FRItriggers on the 13th of the month AND every Friday. - AND (Set to t):
13 * FRItriggers only on Friday the 13th.
- OR (Default):
REPEAT_DEADLINE: (Optional)- If set to t : The Cron rule in
REPEAT_CRONupdates theDEADLINEinstead ofSCHEDULED. (ExistingSCHEDULEDtimestamps are cleared to prevent conflicts). - If set to a Cron expression (e.g.
0 18 * * 5): Enable independent repetition mode. TheSCHEDULEDtime followsREPEAT_CRON, while theDEADLINEfollows this specific expression.
- If set to t : The Cron rule in
REPEAT_ANCHOR&REPEAT_DEADLINE_ANCHOR: Automatically maintained by the plugin to store the last trigger base time. Users usually don’t need to edit this. If deleted, the plugin will use the current time as the base for the next calculation.
- Create a
PROPERTIESdrawer under an Org heading. - Add the
REPEAT_CRONproperty with your expression. - (Optional) Add
REPEAT_DAY_ANDorREPEAT_DEADLINE. - Change the task state to
DONEas usual. - Automatically:
org-repeat-by-croncaptures the state change and provides a temporary repeater to let Org-mode handle standard procedures (logging, keywords, etc.).- org-mode itself handles the repetition event.
org-repeat-by-cronthen recalculates the next valid time based on the Cron rules.- It updates the task’s
SCHEDULEDorDEADLINEtimestamp (or both) and removes the temporary repeater.
Note: Because the repetition is processed by Org-mode, this package is fully compatible with org-habit .
Standard Cron expressions consist of 5 fields separated by spaces.
| Field | Allowed Values | 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 Sunday) or SUN-SAT | * , - / L # |
Note: This parser treats * as the full range and supports logic switching via DAY_AND, so the special character ? is not required.
| Char | Description | Example |
|---|---|---|
| * | Any value | * in Hour means “every hour” |
| , | Value list | 1,15 in Day means “the 1st and 15th” |
| - | Range | MON-FRI in Day of Week means “Monday to Friday” |
| / | Step | */15 in Minute means “every 15 minutes” |
L- Last: In the Day field,
Lmeans the last day of the month. In the Day of Week field,5Lmeans the last Friday of the month. L(Day) -> Jan 31st, Feb 28th, etc.6L(Day of Week) -> The last Saturday of the month.
- Last: In the Day field,
W- Nearest Weekday:
15Wmeans the closest weekday (Mon-Fri) to the 15th. If the 15th is a Saturday, it matches the 14th (Fri). If it’s a Sunday, it matches the 16th (Mon). - It will not cross month boundaries (e.g., if the 1st is Saturday,
1Wmatches the 3rd, Monday).
- Nearest Weekday:
LW- Last Weekday of the month.
#- Nth Day of Week: Format is
DOW#N. 5#2-> The second Friday of the month.1#1,1#3-> The first and third Monday.
- Nth Day of Week: Format is
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:
Reminder to pay bills on the last day of every month (using 3-field format).
* TODO Pay Credit Card Bill
SCHEDULED: <2025-09-30 Tue>
:PROPERTIES:
:REPEAT_CRON: L * *
:END:
Meetings held on the 1st and 3rd Monday of the month.
* TODO Bi-weekly Sync Meeting
DEADLINE: <2025-10-06 Mon 10:00>
:PROPERTIES:
:REPEAT_CRON: 0 10 * * MON#1,MON#3
:REPEAT_DEADLINE: t
:END:
First Monday of the first month of every quarter.
* TODO Server Quarterly Maintenance
SCHEDULED: <2025-10-06 Mon>
:PROPERTIES:
:REPEAT_CRON: * JAN,APR,JUL,OCT MON#1
:END:
Only triggers when the month is the 13th and that day is Friday.
* TODO Black Friday Special
SCHEDULED: <2026-03-13 Fri>
:PROPERTIES:
:REPEAT_CRON: 13 * 5
:REPEAT_DAY_AND: t
:END:
Workout Mon, Wed, Fri. Show habit tracking bars in Agenda, allowing a 2-day delay.
* TODO Strength Training
SCHEDULED: <2025-09-15 Mon .+1d/2d>
:PROPERTIES:
:STYLE: habit
:REPEAT_CRON: * * MON,WED,FRI
:END:
Note: Always keep a repeater like .+1d/2d when using org-habit to preserve the consistency graph.
Suppose you have a weekly task, it should be due every Friday, but you will arrange to complete it on Thursday when free.
* TODO Weekly Key Project
SCHEDULED: <2025-09-08 Mon> DEADLINE: <2025-09-12 Fri>
:PROPERTIES:
:REPEAT_CRON: * * 4
:REPEAT_DEADLINE: * * FRI
:END:
Since the schedule/deadline timestamps obtained during rescheduling are from after the repetition was executed, they have been excluded from the base-time source to prevent the base-time from being incorrectly postponed.
Fixed the issue where temporary repeaters could not be correctly created when there were no schedule/deadline timestamps.
Updated internal logic to use temporary repeaters.
This allows org-mode to natively handle todo-keywords and org-log, avoiding compatibility issues. The package now only resets timestamps after Org finishes its processing.
To maintain compatibility, the previous version removed the previously used when-let*, which inadvertently triggered the org-repeat-by-cron-on-done function again.
- New: Supports independent
DEADLINErepeat rules. Now theREPEAT_DEADLINEproperty can be filled with an independentCronexpression, realizing co-existence scheduling withSCHEDULE. - New: Introduced
REPEAT_DEADLINE_ANCHORto support the calculation baseline for independent deadlines. - Optimization: Internal code refactoring.
Bug fix for org-repeat-by-cron-on-done trigger logic
Fixed an issue where org-repeat-by-cron-on-done was incorrectly triggered on all headings. The function now correctly filters and only triggers on headings that possess the REPEAT_CRON property.
- Added support for native Org repeaters.
org-repeat-by-cronnow stashes therepeater, calculates the new date, and restores the cookie. - Optimized internal logic.
The rescheduled timestamp will include a time component (format %Y-%m-%d %a %H:%M ) if any of the following conditions are met (evaluated in a logical OR sequence):
- 5-field Cron: The
REPEAT_CRONrule has 5 fields, implying a specific time is intended. - Time in Anchor: The
REPEAT_ANCHORproperty already contains a time string (HH:MM). - Time in existing timestamp: The current
SCHEDULEDorDEADLINEtimestamp already contains a time string.
Changed the sequence of L and numbers from L5 to 5L to align with standard Cron conventions.
- Correctly compares
base-timeandcurrent-timeto ensure forward scheduling. - Switched the default trigger hook from
org-after-todo-state-change-hooktoorg-trigger-hookfor better compatibility.
Updated README
basic functionality.