Files
logseq/docs/recurring-tasks.md
Cory Donnelly 2169a86f03 fix: honor documented repeater cookie semantics (#12523)
* 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>
2026-05-14 17:53:11 +08:00

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.

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-completion implements .+
  • advance-from-scheduled implements +
  • advance-until-future implements ++

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.