Skip to content

Repository files navigation

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 setLater 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

---
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 due date, priority, category, repeat rule or asset
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
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.

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.

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:

---
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

  • Recurringlast done = today, due recomputed, done back to false. A recurring task is never left at done: true; that is what makes it recur.
  • One-timelast 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.

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 <vault>/.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:

git tag 0.2.2 && git push origin 0.2.2

Then, on the machine with the other vault:

gh release download 0.2.2 --repo tteschon/obsidian-task-base \
  -D "<vault>/.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

ln -s /path/to/repo <vault>/.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/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 (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.

Development

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, 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

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:

obsidian plugin:reload id=task-base

About

Obsidian plugin for note-per-task management with RFC 5545 recurrence, backed by a Bases file.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages