# Task Base An Obsidian plugin for a note-per-task system: capture a task through a form, pick a cadence from a builder instead of typing RFC 5545, and complete a task with the next due date computed for you. It conforms to an existing schema rather than inventing one. A note is a task because it carries `type: task`; a Bases file collects them from anywhere in the vault. > A community plugin, not affiliated with or endorsed by Obsidian. "Bases" > refers to Obsidian's own feature, which this plugin reads and writes but is > not part of. ## Screenshots The task pane, showing Overdue, Today, This week, Needs attention and Later sections The pane's sections **partition the open set** — `Later` is whatever the other sections did not catch, so nothing can be due-dated into invisibility. It is collapsed by default so the pane opens on what is urgent. The repeat builder, showing presets, a last-day-of-month toggle, and a live preview resolving to 30 September, 31 October and 30 November Recurrence without typing RFC 5545. The preview shows the rule it will store, what that means in English, and the next three dates it resolves to — here 30 September, 31 October, 30 November, because `BYMONTHDAY=-1` means *the last day*, not *the 30th*. The new task form Editing an existing task Capture on the left, edit on the right. Every field stays changeable after creation, and the asset field is a type-ahead over the notes in your vault rather than free text you can typo. The generated task base as an Obsidian Bases table The same tasks in Obsidian's own Bases table. The plugin can create this base for you; it is optional, and the pane works without it. ## How this differs from the other task plugins **No plugin in Obsidian's 7,000-plus community registry mentions RRULE.** That is the distinction: recurrence here is an RFC 5545 rule stored on the note — `FREQ=MONTHLY;INTERVAL=6;BYMONTHDAY=-1` — evaluated with the same library calendar software uses, rather than a bespoke "every 6 months" string. - **Tasks** manages checkboxes written inline in note bodies. Different unit of work entirely; the two do not overlap. - **TaskNotes** is also note-per-task, built around a calendar, pomodoro and time tracking. If you want time tracking, use that. This one is for things that come back on a schedule — maintenance, chores, services — where the note is the record, a Bases file is the view, and the recurrence rule is a standard you can read somewhere else. ## The task note contract ```yaml --- done: false # checkbox due: 2026-12-31 # date created: 2026-08-31 # date priority: low # low | medium | high category: vehicle # home | yard | errands | vehicle | health last done: 2026-06-19 # date — latest completion only frequency: FREQ=MONTHLY;INTERVAL=6;BYMONTHDAY=-1 # RRULE, or empty type: task # identity — this is what puts it in the base asset: "[[Family Car]]" # optional --- ``` `frequency` is the only thing separating the two kinds of task. Set means recurring; empty means one-time. ## Commands | Command | What it does | |---|---| | Create task | Form for name, category, priority, due, repeat rule, asset | | Complete task | Branches on `frequency` — see below | | Edit task | Change the name, due date, priority, category, repeat rule, asset or notes | | Edit repeat rule | Opens the RRULE builder on a task | | Set asset | Attaches, changes, or clears the asset on an existing task | | Create asset note | Creates a `type: asset` note and opens it — no task needed | | Recompute due date from repeat rule | Rolls `due` forward from `last done` | | Reschedule overdue tasks | Moves every overdue due date forward, after a preview — see below | | Open task base | Opens the base file, creating one if there isn't one | | Open task list | The sidebar view | The task pane carries its own toolbar — **New task**, refresh, and a button that opens the base file, **creating one if you don't have a base yet**. Its filters are generated from the **Excluded folders** setting, so a base made this way agrees with the pane by construction. It sticks to the top so the primary action stays reachable once the list scrolls. Right-click any row for Edit / Complete / Open. While anything is overdue, the **Overdue** heading carries a button that reschedules the lot — see [Rescheduling overdue tasks](#rescheduling-overdue-tasks). Its sections — Overdue, Today, This week, Needs attention, Later — **partition the open set**: `Later` is defined as everything not caught by the others, not as a date window, so no open task can be missing from the pane while the footer counts it. `Later` is collapsed by default, with its count on the header. Commands act on the active note when it is a task, and offer a picker when it is not. ## Notes Both create forms end in a **Notes** box that becomes the note's body. On a phone that was the one field you could not see while typing into it: the form is taller than the screen long before the box is reached, so tapping it leaves the box at the bottom of the view with the on-screen keyboard over it. So on a phone the box is not there. In its place is a one-line button carrying the opening of what you wrote, and tapping it opens an editor holding **one field and its heading** — the top of the screen, which is the half the keyboard leaves alone. Collapsing the box lifts every field above it back into view as well, which is the same complaint from the other end. On a tablet or a desktop the inline box stays, with an expand button beside it opening that same editor for anything longer than a line. The editor writes nothing to the vault: it edits the field, and the form still has to be submitted. It commits as you type rather than on **Done**, because Escape, a tap outside it, and the Android back gesture all dismiss a modal, and losing a paragraph to any of the three is not a trade worth making. **Edit task** offers the same box on an existing note, filled from the note's body. It is given the prose and not the completion log: the log is history — one dated line per completion, written over months — and editing a paragraph should not be able to take a service record with it. Everything from `## Service log` down is carried across untouched, and the field says so when there is one. Editing the log itself means opening the note. The box also never rewrites what it did not touch. Save the form without changing the notes and the body is left byte-identical — the same rule the properties have always followed here, extended to the prose. ## Renaming a task **Edit task** carries a **Name** field, which renames the note. It goes through Obsidian's own rename, so every link pointing at the task is rewritten with it — a task renamed out from under the notes that reference it would be a worse outcome than the typo it fixed. A name another note in the same folder already holds is **refused**, not quietly numbered the way a newly created note is. Creating a second "Mow lawn" is normal; renaming a task and silently getting "Mow lawn 1" is not what was asked for. Correcting only a name's capitalisation is not a collision with itself and goes through. The rename runs last, after the properties and the notes are written, and reports on its own. A refused rename therefore costs you the rename — which the notice names — and not the edits made alongside it, which are already saved. The form stays open on it, since the field to correct is the one in front of you. ## Assets A task's `asset` property links the note for the thing being serviced — `asset: "[[Lawn Mower]]"`. **A note is an asset because it carries `type: asset`**, the same rule this plugin uses for tasks. A vault may also collect the same notes by folder — an `Inventory` base filtering on `file.inFolder("Inventory")`, say. The two agree as long as every asset note lives in that folder, and diverge if one is moved out; matching on the property is what keeps the picker working either way. The asset field is a type-ahead: click it and every asset drops down, typing narrows the list. A name with no matching note is still accepted — capturing a chore for something not yet inventoried is normal — but the field says so plainly rather than writing a dangling link in silence. **The field states that rule and the + beside it acts on it.** Where the list came from used to be answerable only by reading this file: an empty dropdown explained neither why it was empty nor what would fill it. The field now says what makes a note an asset, and the button creates one without leaving the form — the new note appears in the list straight away, and its name drops into the field you were filling in. New asset notes go to the **New asset folder** setting, and carry exactly two properties: ```yaml --- type: asset created: 2026-09-03 --- ``` That is the whole contract. `type: asset` is what puts the note in the picker; `created` matches what tasks carry and is a real date, so it sorts. Nothing else is written, because a lawn mower is described with fields no task plugin can guess — model numbers, warranty dates, where it lives — and those are yours to add. The folder is only consulted when *writing* one. Assets are still found anywhere in the vault by their property, so moving a note out of that folder does not stop it being an asset. ## Completing a task - **Recurring** — `last done` = today, `due` recomputed, `done` back to `false`. A recurring task is never left at `done: true`; that is what makes it recur. - **One-time** — `last done` = today, `done` = `true`. The note stays on disk and stays in the base. **Nothing is ever deleted.** - **Unreadable rule** — refuses to act and asks for the rule to be fixed. Completing it as one-time would write `done: true` and retire a task that was meant to keep recurring. Anything that is not a date — mileage, part numbers, what was actually done — goes on a dated line under `## Service log` in the body, never into `last done`. ## How the next due date is computed The policy is **skip the rest of the period you just did it in**, then take the schedule's next occurrence. | Rule | Completed | Next due | |---|---|---| | `FREQ=WEEKLY;BYDAY=MO` | Mon 2026-08-24 | 2026-08-31 | | `FREQ=WEEKLY;BYDAY=SU` | Mon 2026-08-17 | 2026-08-30 | | `FREQ=WEEKLY;INTERVAL=2;BYDAY=MO` | Mon 2026-08-24 | 2026-09-07 | | `FREQ=MONTHLY;BYMONTHDAY=-1` | 2026-08-22 | 2026-09-30 | | `FREQ=MONTHLY;INTERVAL=6;BYMONTHDAY=-1` | 2026-06-19 | 2026-12-31 | | `FREQ=YEARLY` | 2026-06-19 | 2027-06-19 | Three cases, in order: 1. **No day-selection in the rule** (`FREQ=YEARLY`) — the day is implied by the anchor, so the roll is one period later. A yearly task done 19 June comes back the following 19 June, not on 1 January. 2. **More than one occurrence per period** (`FREQ=WEEKLY;BYDAY=MO,TH`) — the remaining occurrences this period are real, so take the strict next one. 3. **One occurrence per period** — anchor the rule at the start of the current period so nothing is clipped, then take the first occurrence after that period ends. `INTERVAL` does the rest of the skipping itself. Case 3 is why this is not a plain `rrule.after(today)`. Anchoring at the completion date makes `INTERVAL` step from there, so an oil change on 2026-06-19 under `FREQ=MONTHLY;INTERVAL=6;BYMONTHDAY=-1` would roll to 2026-06-30 — eleven days out instead of six months. ## Rescheduling overdue tasks **Reschedule overdue tasks** — a command, and a button on the pane's Overdue heading while anything is overdue — moves every overdue due date forward at once. It is not completing: **only `due` changes**. `done`, `last done` and the service log are left alone, because nothing was done. - **Repeating** — `due` moves to the first date in the task's own schedule on or after today. - **One-time** — `due` moves to today. - **Unreadable rule, or a schedule that has ended** (`COUNT`, `UNTIL`) — left as it is. Moving an unreadable rule to today would treat it as one-time, the same guess completing refuses to make. **Nothing is written until you have seen where each task lands.** The dates it replaces cannot be read back from the notes afterwards, so a dialog lists every task with its old and new date first, and the ones it will leave alone with the reason. Each task is decided again from its note at the moment of writing, so one completed while the list was open stays as it was. "The first date in its own schedule" steps the task's chain of due dates — each the date completing it on time would have produced — forward from the date it holds, until it reaches today. That is not the same as completing it today, which spends the current period and restarts an `INTERVAL` from this week: | Rule | Due | Today | Rescheduled to | Completing today gives | |---|---|---|---|---| | `FREQ=WEEKLY;BYDAY=SU` | Sun 2026-09-13 | Tue 2026-09-22 | 2026-09-27 | 2026-10-04 | | `FREQ=MONTHLY;BYMONTHDAY=-1` | 2026-08-31 | Tue 2026-09-22 | 2026-09-30 | 2026-10-31 | | `FREQ=WEEKLY;INTERVAL=2;BYDAY=MO` | Mon 2026-09-14 | Tue 2026-09-22 | 2026-09-28 | 2026-10-05 | | `FREQ=WEEKLY;BYDAY=TU` | Tue 2026-09-15 | Tue 2026-09-22 | 2026-09-22 | 2026-09-29 | An occurrence that falls today makes the task due today rather than skipping past it. **Recompute due date from repeat rule** is different again: it works from `last done`, as though the task had been completed then. Only open tasks due before today are touched. A recurring task stuck at `done: true` is not overdue work — the pane goes on reporting it, and rescheduling leaves it alone. ## Things this plugin will not do - **Delete a note.** Sweeping finished one-time tasks is deliberately out of scope. The sidebar surfaces recurring tasks stuck at `done: true`, and unreadable repeat rules, and the generated base carries a `Sweep` view of finished one-time tasks - but all three only ever report. - **Write `.obsidian/types.json`.** The property-type registry is vault-wide; a stray write there breaks date sorting on every note at once. - **Write to a Kanban board or any hand-maintained table.** Frontmatter is the source of truth. ## Known seam: the base filters are duplicated Bases exposes no documented plugin API, so `src/model/taskRepository.ts` does not query `tasks/task base.base`. It walks the vault and re-implements the base's two filter clauses against the metadata cache: ``` type == "task" and !file.inFolder("Templates") ``` **If the `.base` file's filters change, that file must change with them**, or the sidebar and the base will quietly disagree. **Excluded folders** is a setting for exactly this reason. `src/model/assetRepository.ts` is the same shape for assets, and matches on `type: asset` rather than mirroring `inventory.base`'s folder filter — see **Assets** above for why. ## Installing in another vault A packaged Obsidian plugin is three files — `main.js`, `manifest.json`, `styles.css` — in `/.obsidian/plugins/task-base/`. There is no installer format beyond that. ### From a release (any machine) Pushing a tag that matches `manifest.json`'s version builds and publishes those three files as release assets: ```bash git tag 0.2.2 && git push origin 0.2.2 ``` Then, on the machine with the other vault: ```bash gh release download 0.2.2 --repo tteschon/obsidian-task-base \ -D "/.obsidian/plugins/task-base" ``` Or download the three assets from the Releases page while signed in — the repo being private only means you must be authenticated, not that this stops working. Enable **Task Base** under Settings → Community plugins afterwards; if Obsidian is already running, reload it first so it rescans the plugins folder. The release workflow refuses to publish when the tag disagrees with `manifest.json`, because Obsidian matches a release to an installed plugin by that version and a mismatch installs nowhere. `npm version patch` bumps `manifest.json` and `versions.json` together, which keeps them honest. ### On this machine ```bash ln -s /path/to/repo /.obsidian/plugins/task-base # after npm run build ``` One build then serves every vault. Copying the three files by hand works too, and lets each vault run a different version. ### A new vault needs configuring, not just the files The plugin finds tasks by `type: task` **anywhere in the vault**, so the defaults — which describe one particular vault — are usually wrong somewhere else. Four settings matter: - **New task folder** — where new notes land. Created if missing. - **New asset folder** — where the **+** beside the asset field writes new asset notes. Created if missing, and matched to a folder that already exists under a different capitalisation rather than colliding with it. - **Excluded folders** — any folder whose notes carry `type: task` for an unrelated reason. This is the one that bites: a vault with Kanban cards or another `type: task` convention will otherwise list them in the pane's count and offer them in the **Complete task** picker, where choosing one writes `done: true` and `last done` into a note that was never a task. - **Task base** — clear it, or point it at that vault's base. When the path does not resolve, the pane's "Open task base" button is simply hidden. **No `.base` file is required.** The pane reads the metadata cache directly, so every command and every section works in a vault with no base at all. The base only adds Obsidian's own table view. If you want one, the pane's base button creates it — that is the whole setup. Property types need no setup either: Obsidian infers `done` as a checkbox and `due` / `last done` as dates from the values the plugin writes. The plugin never touches `.obsidian/types.json`. ## `created` is a date, vault-wide `created` used to hold a wikilink to the daily note — `"[[2026-08-31]]"` — which made it **text**, so it could not be sorted, compared, or used in a formula. It is now a bare `YYYY-MM-DD`, registered as `date` in `.obsidian/types.json`. Worth knowing before touching it again: **Obsidian property types are vault-wide, not per-note-type.** `created` was on 259 notes in three incompatible formats — 194 daily notes carrying `["YYYY-MM-DD HH:MM"]`, 36 wikilinks, 23 bare dates — so registering the type meant normalising all of them. Daily notes lost the time component in the process. The plugin writes the bare form, and the vault's task template matches. ## What the plugin reads, and why it reads the whole vault To find tasks it **enumerates the vault's markdown files and reads each one's frontmatter**, keeping those with `type: task` (and `type: asset` for the asset picker). That is inherent to the design rather than incidental: a note is a task because of a property, not because of where it sits, and Obsidian offers no API to query notes by frontmatter property. It reads frontmatter only — never the body of a note it was not asked to open — and the asset list is cached rather than recomputed per keystroke. It **makes no network requests of any kind**, collects no telemetry, and never deletes a note. Everything it writes goes through the vault API, inside the vault. If you would rather it looked at less, **Excluded folders** narrows what it considers. ## A hazard worth knowing **Do not name a method on the view class after a Workspace lifecycle verb.** `View.open()` is a real method Obsidian calls to open a view, but it is absent from `obsidian.d.ts` — so defining `private open(task: Task)` on the view type checks cleanly, overrides Obsidian's own method, and the pane then renders blank: the view constructs, `onload` and `onOpen` never run, and **nothing is logged anywhere**. It is named `openNote` for that reason. `render()` catches its own failures and paints the error into the pane, so the next render bug reports itself rather than showing an empty panel. ## Layout ``` src/ main.ts commands, view registration, ribbon settings.ts settings + tab dates.ts all-day date helpers recurrence.ts RRULE parse / describe / next-due / build rruleCompat.ts CJS/ESM interop shim for the rrule package model/frontmatter.ts pure text: note rendering, YAML normalisation model/assetLink.ts pure text: asset wikilink <-> bare name model/completion.ts pure: the branch completing a task takes model/reschedule.ts pure: where rescheduling moves an overdue task model/task.ts the field contract, read + write model/asset.ts write an asset note model/note.ts placing a new note: folder, casing, name collisions model/taskRepository.ts find and bucket tasks model/assetRepository.ts find asset notes settingsData.ts pure: settings shape, defaults, migration ui/ modals and the sidebar view test/ node:test suites over the pure modules ``` `model/frontmatter.ts` holds everything that does not touch the Obsidian API, which is what makes the test suite runnable outside the app — and those are the functions most worth testing, since a regex that strays out of the frontmatter would rewrite someone's note body. ## Third-party code `main.js` is a bundle: it contains [rrule](https://github.com/jakubroztocil/rrule) (BSD-3-Clause). Its notice travels in the banner at the top of every built `main.js` as well as in [THIRD-PARTY-NOTICES.md](THIRD-PARTY-NOTICES.md). ## Development ```bash npm install echo "$HOME/path/to/YourVault/.obsidian/plugins/task-base" > .vault-plugin-dir npm run dev # watch build, writes straight into the vault plugin folder npm test # node:test over recurrence, completion, rescheduling, frontmatter, asset links, settings npm run lint # eslint npm run build # typecheck + minified build to the repo root, for release ``` The dev build destination is per-machine and not committed. It comes from `.vault-plugin-dir` (gitignored) or from a `VAULT_PLUGIN_DIR` environment variable, which wins if both are set. `npm run dev` writes `main.js`, `manifest.json` and `styles.css` there so an Obsidian reload picks the change up, and exits with an error if neither is set rather than quietly building somewhere you are not watching. `npm run build` ignores both and writes to the repo root. After the first build, enable **Task Base** in Settings → Community plugins. ### Cutting a release ```bash npm version patch && git push --follow-tags ``` `npm version` bumps `manifest.json` and `versions.json` together and creates the tag; `--follow-tags` pushes the commit and the tag as one step. **Bumping without tagging breaks installs silently** — the directory reads the manifest to learn the current version and then downloads the release tagged with it, so a listing that looks healthy fails for everyone. The `Release drift` workflow warns on push and fails on its daily run if the two ever disagree. Reload it after a rebuild with the Obsidian CLI: ```bash obsidian plugin:reload id=task-base ```