---
name: obsidian-tasks
description: Creates and manages task notes in an Obsidian vault - one note per task, identified by a type property set to task and collected by a Bases file, carrying a done checkbox, due, priority, category, and an RRULE frequency. Use this skill when the user wants to add a task, mark one done, change its priority or due date, roll a recurring chore forward after doing it, or ask what to work on next - what are my top tasks, what is overdue, what should I do this weekend, add mowing the lawn to my tasks, I just changed the oil. Also use it when the user mentions their task base, a task repo, a task note's done state or frequency, or a backlog of home, yard, or vehicle chores. Do not use it for checkbox tasks inline in note bodies - what the obsidian tasks command lists - do not use it to review the whole base or to sweep out finished tasks - groom my tasks, what is stale - which is obsidian-task-grooming; and do not use it for general vault reading, searching, or note editing - obsidian-vault covers those.
compatibility: Requires the obsidian CLI and a vault whose task notes carry a type property of task, collected by a Bases file
---
# Obsidian Tasks
Run a note-per-task system in an Obsidian vault - create tasks, move them
through their lifecycle, roll recurring chores forward after they are done,
and answer what to work on next.
**A note is a task because it has `type: task`, not because of where it
lives.** The base collects them wherever they sit. Every write here changes
the user's real notes - one of them deletes a note - and the CLI exits 0 on
failure, so nothing is done until the read-back in Step 7.
## Before you start
`obsidian-vault` covers the CLI itself - preflight, vault targeting, and why
exit codes cannot be trusted; do not re-derive it. Run its check, then
**resolve the layout rather than assuming it**:
```bash
command -v obsidian # exit 1 - stop, not fixable from the shell
command -v uv # needed to evaluate RRULEs in Step 4
obsidian vault info=name # confirm the right vault
obsidian bases # find the task base
```
Rolling a recurring task forward evaluates an RFC 5545 `RRULE` and so needs
Python as well as the CLI - `uv run --with python-dateutil` supplies it per
invocation, with no project dependency. Every other step is shell only.
| Found | Do |
|---|---|
| A task base | Use the path `bases` printed, exactly |
| Several bases | Pick by name, or ask; do not guess |
| No task base | Confirm with the user, then build the one in `references/base.md` |
**Use the path the app prints, never the one `ls` shows** - paths are
case-sensitive to the CLI and case-insensitive on macOS disk. See Gotchas.
Then `obsidian read path=""` to learn its filter, which names the
property marking a task and any folder the base restricts to.
## Step 1 - Survey and learn the schema
Read current state before any write, and let the existing rows define the
field set rather than a list baked into this skill:
```bash
obsidian base:query path="" format=json
```
One object per task, keyed by the base view's columns. **The keys are the
schema** - a new task should carry the same ones. Never assume a value from
memory or from the user's phrasing.
Formula columns come back with the properties. When the base defines
`days_until_due` and `overdue`, **those values are the answer** - the vault
computed them, so do not recompute from `due`. `base:views` lists what a base
offers, and `base:query ... view="Today"` queries one directly. A base
predating those views has neither; Step 6's rules still apply.
**The base includes done tasks.** Its filter no longer excludes them, so
`base:query` is the full inventory rather than a list of open work. Two
consequences: drop `done: true` rows before ranking anything (Step 6), and
recognise them as `obsidian-task-grooming`'s sweep queue rather than work.
## Step 2 - Create a task
`base:create` satisfies the base's filters on its own - it stamps the filter
property into the new note and writes the file into the folder the filter
names, or beside the `.base` file when it names none. The note comes back
scaffolded: every column in the view's `order:` list as an empty key, with
`type: task` filled in. Fill in what the user gave, plus `created`, which
arrives empty because `base:create` takes no template:
```bash
obsidian base:create path="" name=""
obsidian property:set name=done value=false type=checkbox path=""
obsidian property:set name=priority value=medium path=""
obsidian property:set name=category value=yard path=""
obsidian property:set name=created value="[[]]" path=""
```
`created` is the daily-note backlink - a quoted wikilink, never a bare date.
The user's template sets it; `base:create` does not.
**Never pass `type=` to `property:set`** - see Gotchas. It is doubly
confusing here, because `type` is also the property that marks a task:
`name=type` is correct, `type=text` is the destructive one.
Ask for `category` and `priority` when the user did not say; leave `due` and
`frequency` empty rather than inventing them. If `base:create` fails, fall back
to `create name="" path=""` then
`property:set name=type value=task` - the note is only a task once it has
that property.
Read `references/schema.md` before creating or migrating: it holds the field
contract and the values already in use, so a new task reuses a `category`
instead of coining one.
## Step 3 - Complete a one-time task
Completion is the `done` checkbox. There is no in-flight state; a task is done
or it is not.
**Check `frequency` before writing `done: true`** - it separates the two kinds
of task, and the branch is not recoverable by reading the note afterwards. A
task *with* a rule goes to Step 4 and never gets `done: true`. A task
*without* one is finished for good here, and finishing it removes the note:
```bash
obsidian property:set name="last done" value="" path=""
obsidian property:set name=done value=true type=checkbox path=""
obsidian delete path="" # prints: Moved to trash:
```
**`done` is the one property that needs `type=checkbox`** - without the flag
the CLI writes the string `"false"` instead of a boolean, and every filter
that tests it stops working. See Gotchas; the blanket ban on `type=` still
holds for every other property.
**Confirm with the user before the `delete`, naming the note.** It is the one
command in this skill that removes work. Write `done: true` first even
though the note is about to go: the trashed copy is recoverable, and it
should read as finished work rather than as an abandoned draft.
Deleting is right only because the task is one-time. Anything the user may
want later - what was actually done, a measurement, a receipt - belongs in a
note that outlives the task, so offer to move it before deleting rather than
after. The ones already sitting in the base are swept by
`obsidian-task-grooming`, not here.
## Step 4 - Complete a recurring task
A recurring task is never left `done`; that is what makes it recur. Marking
one `done` and stopping is still the most likely mistake in this skill, but it
no longer hides: done tasks stay in the base, so a done row carrying a
`frequency` is visible as the anomaly it is. `obsidian-task-grooming` lists
those rows for roll-forward and never deletes them - **`frequency` is what
keeps a task out of the sweep**, which is one more reason never to clear it.
`frequency` holds an RFC 5545 `RRULE`. The new `due` is the next occurrence
**strictly after today**, anchored on the task's current `due`:
```bash
uv run --with python-dateutil python3 -c '
import sys, datetime as d
from dateutil.rrule import rrulestr
r = rrulestr(sys.argv[1], dtstart=d.datetime.fromisoformat(sys.argv[2]))
print(r.after(d.datetime.fromisoformat(sys.argv[3])).date().isoformat())
' "" "" ""
```
**Never compute this by hand.** RRULE's `BY*` parts expand or limit depending
on the `FREQ` above them, and the intuitive spelling of "annually on the last
day of the month" - `FREQ=YEARLY;BYMONTHDAY=-1` - silently yields a *monthly*
series. `references/recurrence.md` has the value grammar, the traps, and the
verified cases.
Anchoring on `due` is what makes a late completion land on the next scheduled
slot instead of shifting every future cycle. Confirm afterwards that the new
`due` is itself on the rule's grid - an off-grid anchor rolls forward by days
instead of months.
Then write three properties, in this order, and append to the body:
```bash
obsidian property:set name="last done" value="" path=""
obsidian property:set name=due value="" path=""
obsidian property:set name=done value=false type=checkbox path=""
obsidian append path="" content='\n- - \n'
```
`last done` and `due` are real dates, always. Non-date detail - mileage, a
part number, what was done - goes on the body log line under a
`## Service log` heading, **never** into those two fields. The body log also
keeps the history that `last done` overwrites.
Read `references/recurrence.md` before writing or editing any rule. It holds
the `RRULE` value grammar, the evaluator invocation, the grid-alignment
assertion, and the expand-versus-limit traps that make hand-computed dates
wrong without erroring.
## Step 5 - Sweeping the base belongs to grooming
Finished one-time tasks stay in the base rather than vanishing, and clearing
them out is `obsidian-task-grooming`'s Step 3: it surveys the whole base, lists
every candidate by name, and deletes on a single confirmation. Hand off to it
rather than sweeping here.
The rule that decides what may go - **`frequency` empty means sweepable,
`frequency` set means never delete, roll it forward instead** - lives there and
nowhere else. Do not re-derive it in this skill; two copies of that guard are
two things that can drift apart, and the failure mode is a deleted recurring
schedule.
Step 3 above is a different thing and stays here: it removes the one task the
user finishes or abandons in front of you, named in the conversation. That is
a single note, not a pass over the base.
## Step 6 - Answer what to work on
**Drop `done: true` rows before ranking.** They sit in the base and they are
not work; scheduling one is the way to get this step wrong. Test the value
properly - `base:query` returns `done` as a string, so `if row['done']:` is
true for an *open* task. See Gotchas.
The base's Table view sorts by `done` ascending, then `due` ascending. That
front-loads open work and the soonest due dates, but it is not a priority
ranking. Any ranking is this skill's, so state the rule rather than implying
the vault supplied it.
Default ranking, highest first:
1. Overdue - `overdue` is true, or `due` is a date before today
2. `priority: high`, then `medium`, then `low`
3. Ties broken by `due`, empty `due` last
Prefer the base's own `overdue` and `days_until_due`, and query
`view="Today"` or `view="This week"` rather than filtering every row by hand.
Both now carry a `done != true` clause, so done tasks no longer come back in
them.
Say how many tasks were considered, counting open ones only. A count that
drops between runs means a task was completed and swept, not that something
went missing. Recurring tasks with no `due` are waiting on their first
completion - list them separately rather than as overdue.
## Step 7 - Verify, then report
The CLI exits 0 on failure, so check the output text and then the data:
1. Each write prints `Created: `, `Set : `, or
`Moved to trash: `. A line starting with `Error: ` is a failure -
including `Error: File "..." not found.`, which is what a delete against a
mistyped path prints in place of doing anything.
2. Re-run the Step 1 `base:query`. Done tasks stay in the base now, so absence
has one innocent cause and one failure:
| Absent from `base:query` | Meaning |
|---|---|
| You deleted it in Step 3, or grooming swept it | Correct - `obsidian read` on the path confirms it, printing `Error: File "..." not found.` |
| Anything else | **The note has no `type: task`** - the classic failure |
The second is invisible from the note alone, which is why the read-back
goes against the base and not the file. A completed one-time task that is
still *present* means the delete did not happen - check its output line.
Never report a change as made on the strength of a silent command. Then give
the note path, the fields changed, and their new values, quoting the
read-back. For a roll-forward, state the new `due` and how it was computed.
For a deletion, name every note removed and say it is recoverable
from the vault trash. If a command printed `Error: `, say what did not happen.
## Gotchas
- **Never pass `type=` to `property:set`.** It rewrites the property's type
vault-wide in `.obsidian/types.json`, not on the note being edited.
`property:set name=due value="~25,731 mi" type=text` flips `due` from
`date` to `text` for every note and for the base, silently breaking date
sorting everywhere. Omitting `type=` writes the same value and leaves the
registry untouched.
- **Paths are case-sensitive to the CLI but not to the macOS disk.** A vault
folder renamed to `tasks/` in Obsidian still shows as `Tasks/` in `ls`, and
`base:query path="Tasks/task base.base"` fails with `Base file not found`
while the lowercase spelling returns every row. Always take paths from
`obsidian bases` and `obsidian files`, never from the filesystem.
- **Flag sets vary by CLI build, and `obsidian help` on the machine wins.**
Published documentation describes a `silent` flag that this build does not
have; here `open` is an opt-in instead. Check `obsidian help `
before using a flag taken from any external source.
- **`obsidian tasks` is a different system.** It lists checkbox tasks written
inline in note bodies, and its own flags happen to be named `done`, `todo`,
and `status=""` - unrelated to this schema's `done` property despite
the collision. This skill never uses it: a task here is a note with
`type: task`, not a `- [ ]` line.
- **The base holds done tasks too.** The filter no longer excludes
`done: true`, so `base:query` is the full inventory rather than a list of
open work. Drop done rows before ranking, and never schedule one as if it
were outstanding. Clearing them out is grooming's sweep, not this skill's.
- **`done` is the one property that must carry `type=checkbox`.** Without the
flag, `property:set name=done value=false` writes the *string* `"false"`,
and every filter testing it silently stops working. This is the sole
exception to the ban below; `done` is task-exclusive, so registering it
vault-wide is the intent rather than a side effect. Never generalise the
exception to any other property.
- **`base:query` returns `done` as a string, and `"false"` is truthy.** An
open task reads back as `'false'`, a completed one as `'true'` - both
non-empty strings. `if row['done']:` is therefore true for *every* task, and
code built that way treats the whole base as finished. Test
`row['done'] in (True, 'true')`. Verified live: the naive test counted 29 of
29 tasks as done when exactly one was.
- **`frequency` is empty on a one-time task, not missing.** The template
writes the key with no value, so `frequency:` appears on every task note and
a test for an absent line matches nothing - a branch built that way sends
every task down the one-time path. `base:query` returns `null` for an empty
key and for a genuinely absent one alike, which is why the queried value is
the one to test.
- **Never seed an empty `frequency` with `property:set value=""`.** That
writes `frequency: ""`, and an empty string is **not** `null` to Bases, so
every `frequency != null` filter starts matching one-time tasks and the view
returns wrong rows with no error. The template's bare `frequency:` key is
null; let the template write it.
- **A new property is invisible to `base:query` until the base names it.**
Results are keyed by the view's columns, so a property written to every note
reads back as absent until it is added to the `.base` file. Verify a new
field against `obsidian read`, or update the base first.
- **`obsidian delete` trashes by default; never pass `permanent`.** Plain
`delete` prints `Moved to trash: ` and the note stays recoverable from
the vault trash until the user empties it. `permanent` skips that, and
nothing in this skill needs it. Deletion is also the one action here to
confirm with the user before running.
- **Text in `due` breaks computation silently, not loudly.** A formula over a
non-date `due` errors, but a comparison like `due < today()` returns
**`false`** - so the task is never flagged overdue and date-window views
omit it with no error at all. Both fields are real dates on every task
today; keep them that way.
- **`created` is a wikilink, not a date** - `"[[2026-08-16]]"`, pointing at
the daily note. Writing a bare date breaks that backlink. The template
handles it; do not set `created` by hand.
- **Never read or write task state from a Kanban board or a hand-maintained
table.** They look authoritative and drift from the frontmatter;
`references/schema.md` measures the drift and explains why the board is
still a legitimate way to *create* tasks.