M A N D A L I V I A
Obsidian Lab All Notes

Weekly Project Review with Claude Code and Obsidian CLI

I manage projects in Obsidian using a GTD-inspired system: each project is a markdown file with a standard structure, status managed through tags, and activity tracked through dated log entries. It works — until you have a dozen open projects and no systematic way to review them as a portfolio. Individual project catchups keep each one moving, but nothing tells you which projects have gone quiet, which have no next actions, or which should have been put on hold weeks ago.

So I built a weekly review skill for Claude Code that scans every open project, classifies it, and walks me through triage decisions.

How Projects Work in the Vault

Every project starts from a Templater template. The frontmatter carries the metadata that matters:

tags:
  - Projects/Open       # status: Open, Inbox, Hold, Dropped, Done
area: next-act           # life area the project belongs to
review-cycle: 7          # days before the project is considered stale
start-date: 2026-01-15
complete-date:            # set when marked done

The body follows GTD’s natural planning model:

  • Purpose — why this project exists
  • Outcome — what “done” looks like
  • Current Status — the bookmark, updated each session with As of [YYYY-MM-DD](/obsidian/yyyy-mm-dd/)
  • Task Library — all tasks in Obsidian Tasks format (- [ ] checkboxes)
  • Log — chronological entries with ### [YYYY-MM-DD](/obsidian/yyyy-mm-dd/) headings, newest first

Status flows through tags: a project starts as Projects/Inbox, gets promoted to Projects/Open when you commit to it, and eventually moves to Projects/Done, Projects/Hold, or Projects/Dropped. The tag is the single source of truth — Dataview queries, the Obsidian CLI, and the weekly review all key off it.

The Problem with Per-Project Catchups

I already had a /catchup skill that reads a single project file, asks what’s happened since the last log entry, updates status, and suggests a next action. It works well for the project you’re thinking about. It can’t tell you what you’re not thinking about — the project that quietly went stale three weeks ago while you were focused elsewhere.

A weekly review needs to see the whole portfolio at once.

What the Weekly Review Skill Does

The skill runs in four phases.

Phase 1: Gather

A shell script pulls all Projects/Open and Projects/Inbox files via the Obsidian CLI, then reads each file directly to extract:

  • Last log date (from ### [YYYY-MM-DD](/obsidian/yyyy-mm-dd/) headings)
  • Count of incomplete tasks (- [ ] lines)
  • The review-cycle and area properties from frontmatter
  • Whether Current Status has meaningful content

It classifies each project:

ClassificationCriteria
ActiveLog entry within review-cycle, has open tasks
StaleLast log older than review-cycle days
No next actionsNo incomplete tasks, project isn’t done
InboxStill in Projects/Inbox, needs initial decision

A project can be both stale and have no next actions — the script flags both. The output is JSON, one object per project with all the data the LLM needs to present a summary without further file reads.

Phase 2: Present

The summary groups projects by classification. Something like:

Active (2)
  Home renovation — today, 3 tasks
  Launch blog — 2 days ago, 5 tasks
 
Stale (4)
  Spanish taxes — Jan 7, 55 days, no tasks
  Investment rebalance — Feb 16, 15 days, no tasks
  ...
 
Inbox (2)
  Gmail integration — no log, no tasks
  Conference prep — no log, no tasks

Each project shows its area, last log date, days since activity, and open task count. Active projects are listed for awareness; flagged projects are what you’ll triage.

Phase 3: Triage

Walks through flagged projects one at a time, starting with inbox, then stale, then no-next-actions. For each:

  • Keep open — define a next physical action, add a log entry
  • Hold — swap the tag to Projects/Hold, optionally set a revisit date
  • Drop — swap to Projects/Dropped, add a closing log entry
  • Done — swap to Projects/Done, set complete-date, log it

Tag changes preserve all non-status tags. If a project is tagged [Projects/Open, Area/Work], only the status tag gets swapped — area tags stay. The Obsidian CLI handles property writes:

obsidian vault=MyVault property:set name=tags value="Projects/Hold" type=list path=<file>

Log entries follow the vault’s date-linking convention:

### [2026-03-04](/obsidian/2026-03-04/)
- Moved to hold during weekly review. Revisiting after tax season.

Phase 4: Wrap Up

Summarizes what changed — how many moved to hold, how many dropped, how many got new next actions — and reports how many projects remain open.

The Gather Script

The key design decision was moving data extraction into a shell script rather than having the LLM do it inline. The first run of the skill took multiple LLM turns: calling the Obsidian CLI for file lists, then grepping each file individually, then classifying results. Slow, token-heavy, and inconsistent across runs.

The script does it in one call:

  1. Calls obsidian vault=MyVault tag name=Projects/Open verbose to get file paths (one CLI call per status tag — two total)
  2. Reads each file directly with grep and awk for log dates, task counts, and current status content
  3. Extracts properties from YAML frontmatter directly — no per-file CLI calls
  4. Classifies each project and outputs a JSON array
[
  {
    "name": "Investment rebalance",
    "path": "Planner/Projects/PR-2025-Reorient investments.md",
    "status_tag": "open",
    "area": "finance",
    "last_log": "2026-02-16",
    "days_ago": 15,
    "review_cycle": 7,
    "open_tasks": 0,
    "has_current_status": false,
    "classification": "stale,no-next-actions"
  }
]

The LLM consumes the JSON, formats the summary, and moves straight to triage. Phase 1 went from roughly four tool calls to one.

Why Not Use the Obsidian CLI for Everything?

The Obsidian CLI’s property:read command reads one property from one file at a time. For twelve projects and three properties each, that’s 36 CLI calls — each launching a process, connecting to the running Obsidian instance, and returning a value. Reading the file directly with grep is an order of magnitude faster.

The CLI is still the right tool for writing properties during triage (it handles YAML serialization correctly) and for the initial file listing (it resolves tags through Obsidian’s index). The split is: CLI for tag lookups and property writes, direct file reads for bulk data extraction.

Skill File Structure

The skill follows a pattern shared by other Claude Code skills in the vault:

.claude/skills/weeklyreview/
├── SKILL.md                        # Thin wrapper: name, description, pointer to prompt
└── scripts/
    └── gather_projects.sh          # Data extraction + classification
 
Planner/Prompts/
└── SKILLPROMPT-WeeklyReview.md     # Full workflow instructions

The SKILL.md is what Claude Code registers as a slash command. It points to the prompt file, which contains the detailed phase-by-phase instructions. Keeping the prompt in the vault (not buried in .claude/) means it’s editable in Obsidian alongside other notes.

What I Haven’t Built Yet

The triage phase is designed but only lightly tested. The tag-swap logic — read existing tags, find the project status tag, replace only that one, write back the full list — is where things tend to get fiddly with YAML list serialization.

I also want this to feed into a weekly note eventually: a summary of what moved, what’s still open, maybe a portfolio health score over time. That’s a separate piece.

Caveats

Obsidian CLI required. The gather script and triage actions depend on the Obsidian CLI being installed and Obsidian running. Without it, you’d need to parse frontmatter and write properties through direct file manipulation — doable but messier.

Review-cycle defaults matter. If a project doesn’t set review-cycle in frontmatter, the script defaults to 14 days. For fast-moving projects that’s too generous; for slow-burn research projects it’s too aggressive. Set it per-project.

Template discipline. The whole system depends on projects following the template structure — specifically ### [YYYY-MM-DD](/obsidian/yyyy-mm-dd/) log headings and - [ ] task format. Projects that predate the template or use different conventions show up as stale with zero tasks regardless of actual activity.

Environment variable for vault path. The vault path (especially on macOS with iCloud sync) is long and awkward. Setting an $OBSIDIAN_VAULT environment variable in your shell config and referencing it in scripts keeps paths manageable. The gather script falls back to a hardcoded default if the variable isn’t set.

Keep Exploring