* fix: honor the three documented repeater cookie semantics
Logseq's documented repeater semantics (per docs.logseq.com and
`logseq/docs` `Tasks.md`) define three org-mode-style cookies for
recurring tasks:
`.+`: repeats from the last completion date
`++`: advances from scheduled, skipping in whole intervals to future
`+`: advances from scheduled by the declared interval (can stack)
The scheduler in `worker/commands.cljs` has been ignoring the cookie
entirely and applying a single, `++`-like semantic for every
recurring task. A user who wrote `.+1w` in markdown — expecting "a
week from when I actually finished" — silently got `++1w` behavior
("a week from the original scheduled date, skipped to future"),
which for a weekly task scheduled 2026-04-01 and completed on
2026-04-05 returns the next occurrence on 2026-04-08 rather than
the documented 2026-04-12.
This change:
* Adds a closed-values `:logseq.property.repeat/repeat-type` property
with values `:dotted-plus` / `:plus` / `:double-plus`. Default is
`:double-plus` so existing recurring tasks see no behavior change
on upgrade.
* Rewrites the scheduler to branch on repeat-type and implement each
semantic: `.+` anchors on now; `+` advances from original once (can
stack overdue, per org-mode); `++` iterates in whole intervals
until strictly after now. The `++` path is mathematically
equivalent to the previous scheduler, so default-path behavior is
preserved.
* Guards against frequency <= 0 — the old code would silently produce
nonsense and, under the new `++` loop, would spin forever. The
guard short-circuits to `nil`.
* Extracts `resolve-recur-frequency` and fixes the previous
`(or [A B] [C D])` pattern in `compute-reschedule-property-tx` —
any 2-vector is truthy in Clojure, so the default-value branch
was unreachable and entities without an explicit
`:recur-frequency` silently fell through to `frequency = nil`.
`if-let` makes both branches reachable so default-to-1 actually
works at migration time.
* Restores the cookie-type selector that was removed from the
date-time popover in `0a5b88467` (Nov 2020) — in-code support for
all three cookies has been present but not user-pickable for the
last ~5.5 years.
* Adds `docs/recurring-tasks.md` — a technical spec for contributors
and users that restates and augments the upstream `Tasks.md` text,
adds decision guidance, and documents the implementation surface.
* Extends the file-graph → DB-graph migration (built on top of
`44d6bd49c4` "fix: preserve repeated schedule import") to also
carry the cookie kind via a new `repeat-types` map in
`graph-parser/exporter.cljs`, so an imported `.+1w` task lands in
the DB-graph with `:repeat-type.dotted-plus` rather than picking
up the `:double-plus` default. Test updated to assert this.
* Adds deftests covering each cookie's distinctive behavior plus
boundary cases (non-positive frequency, unknown unit, frequency > 1
variants, `++` at month/year units, and both branches of
`resolve-recur-frequency`).
The preexisting `get-next-time-test` passes unchanged under the
`:double-plus` default, preserving the existing regression contract.
Tests pin `t/now` via `with-redefs` for determinism.
Refs #7731, #11260, #6715, #8531. Folds in the small remaining delta
from #12532 (now closed as superseded by `44d6bd49c4`).
* fix: harden recurring task repeat type
* fix: contain repeat type selector
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
* fix: handle clamped monthly repeats
---------
Co-authored-by: Tienson Qin <tiensonqin@gmail.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
4.9 KiB
Recurring Tasks
Logseq supports recurring tasks with three scheduling semantics, modeled on org-mode's three repeater cookies. Picking the right one lets you say exactly how the next occurrence should be computed when you mark a task DONE.
This document is the reference for contributors working on the
scheduler and for users who want the full behavior specification. The
short, user-facing version lives in
logseq/docs Tasks.md.
The three semantics
.+ — Advance from completion
Repeats from the last time you marked the block done.
The next occurrence is one interval after the moment you marked the task DONE, regardless of what the original scheduled date was.
Best for: habits and cadence-driven recurrences — things where the clock restarts the moment you finish.
Example: A weekly task scheduled for 2026-04-01, completed on 2026-04-05, will next appear on 2026-04-12. If you'd forgotten and completed it on 2026-04-20, it would next appear on 2026-04-27. The interval is anchored to your completion, not to the calendar.
++ — Advance from scheduled, skip to future
Keeps it on the same day of the week.
The next occurrence starts at original + one interval and keeps advancing in whole intervals until the result is strictly in the future. Because the arithmetic is in UTC and advances by whole weeks (or months, or years), weekly recurrences naturally land on the same day-of-week as the original.
Best for: calendar-anchored recurrences where completion is a side event and the anchor matters — "every Monday standup," "monthly review on the first of the month."
Example: A weekly standup scheduled for Monday, 2026-04-06:
- Completed Wednesday, 2026-04-08 → next occurrence Monday, 2026-04-13.
- Completed Friday, 2026-04-17 → next occurrence Monday, 2026-04-20.
This is the default when a recurring task has no explicit cookie.
+ — Advance from scheduled, stacking
Repeats in X y/m/w/d/h from when you originally scheduled it.
The next occurrence is exactly original + one interval. If completion was much later than the original date, the next occurrence may land in the past — which causes the task to appear overdue immediately. Org-mode calls this "stacking."
Best for: obligations tied to an exact calendar date that should not shift when you're late — monthly rent, annual renewals, fixed billing cycles.
Example: Rent due 2026-04-01, paid 2026-04-05 → next reminder 2026-05-01. Paid 2026-04-25 → next reminder still 2026-05-01.
Choosing the right cookie
A decision table for picking the right semantic:
| Intent | Cookie |
|---|---|
| "Every 7 days from when I last did it" | .+ |
| "Every Monday" | ++ |
| "The 1st of every month" | + |
Default and backward compatibility
Recurring tasks without an explicit repeat-type default to ++.
This preserves the behavior that existed before the three cookies were
honored independently; no existing task changes on upgrade. Users who
want .+ or + semantics can pick them via the repeat-setting
popover on the task.
Implementation reference
Scheduler
The scheduler lives in src/main/frontend/worker/commands.cljs. It
dispatches on repeat-type via repeat-next-timestamp:
advance-from-completionimplements.+advance-from-scheduledimplements+advance-until-futureimplements++
The dispatch falls back to ++ for nil or unknown values, so the
scheduler is safe against missing or future-added cookie types.
Frequencies of zero or less short-circuit to nil before dispatch to
avoid infinite loops.
Property
:logseq.property.repeat/repeat-type is a closed-values property
declared in deps/db/src/logseq/db/frontend/property.cljs. Its closed
values are:
:logseq.property.repeat/repeat-type.dotted-plus—.+:logseq.property.repeat/repeat-type.plus—+:logseq.property.repeat/repeat-type.double-plus—++(default)
UI
The repeat-setting popover in
src/main/frontend/components/property/value.cljs exposes a "Next date"
selector bound to the repeat-type property. It appears whenever the
task's repeat checkbox is enabled.
Tests
Scheduler tests live in
src/test/frontend/worker/commands_test.cljs. Each of the three
semantics has a dedicated deftest; the preexisting
get-next-time-test exercises the default (++) path. Time is pinned
via with-redefs [t/now ...] for determinism.
Related reading
- End-user docs: logseq/docs
Tasks.md - Org-mode spec: Repeated tasks
- Tracking issues: #7731, #11260, #6715, #8531