wiki-dashboard — Claude Code skill: Obsidian dashboards
Builds live tables and card views over your Obsidian vault: folder and tag filters, grouping, computed columns. Useful when you want a vault index that keeps itself current.
npx skills add Ar9av/obsidian-wiki --skill wiki-dashboard -a claude-codeWhat wiki-dashboard does
The skill covers two tools. Obsidian Bases are native .base YAML files, no plugin required, and you shape the view through the GUI. Dataview is a community plugin with SQL-like syntax and more reach: GROUP BY and computed columns. The skill checks which one you have and prefers Bases unless you ask for Dataview or need the extra features.
Setup runs through the config protocol in llm-wiki/SKILL.md: an inline @name override first, then a walk up the working directory for .env, then global config, then a setup prompt. That yields OBSIDIAN_VAULT_PATH, and the skill reads index.md to see which categories and pages exist. If you have not said what to view, it asks — folder, tag, category, date range.
The part that bites is filter syntax in Bases. Filters are expression strings, not typed objects, and a bare list throws a parse error about needing one of and/or/not. Wrap every filter in and:, or: or not:. Also keep groupBy inside the view definition, and drop the grouped property from order: or the column appears twice. Computed columns live under formulas, for example days_stale as floor((now() - note.updated) / 86400000).
What it helps with
- list every note in the
conceptsfolder with tags and update date - group pages by tag without duplicating the column
- compute how many days have passed since a note was updated
- render entity cards from the
entitiesfolder - filter projects by folder and the
activetag at once
Example prompts for the agent
- Build a dashboard over the
conceptsfolder with name, tags, summary and updated columns - Make cards from the
entitiesfolder with title, tags and summary - Add a computed days_stale column based on the update date
How to install wiki-dashboard
1Skills CLI (Vercel)
npx skills add Ar9av/obsidian-wiki --skill wiki-dashboard -a claude-code2Manually: put the files in place
~/.claude/skills/wiki-dashboard/SKILL.mdCopy the whole skill folder; for one project use .claude/skills/wiki-dashboard/
1Skills CLI (Vercel)
npx skills add Ar9av/obsidian-wiki --skill wiki-dashboard -a codex2Manually: put the files in place
.agents/skills/wiki-dashboard/SKILL.mdFor all projects use ~/.agents/skills/
1Skills CLI (Vercel)
npx skills add Ar9av/obsidian-wiki --skill wiki-dashboard -a cursor2Manually: put the files in place
.cursor/skills/wiki-dashboard/SKILL.mdCursor also reads .agents/skills/ and .claude/skills/
1GitHub CLI
gh skill install Ar9av/obsidian-wiki wiki-dashboard2Manually: put the files in place
.github/skills/wiki-dashboard/SKILL.md1Skills CLI (Vercel)
npx skills add Ar9av/obsidian-wiki --skill wiki-dashboard -a gemini-cli2Platform CLI
gemini skills install https://github.com/Ar9av/obsidian-wiki --path .skills/wiki-dashboardManually: put the files in place
.devin/skills/wiki-dashboard/SKILL.mdThe legacy .windsurf/skills/ path still works
Skills CLI (Vercel)
npx skills add Ar9av/obsidian-wiki --skill wiki-dashboard -a opencodeClaude.ai
Zip the folder and upload it in Settings → Capabilities → Skills
Step by step, with commands for each platform: How to install skills in Claude Code: five ways
Questions about wiki-dashboard
How do I install wiki-dashboard in Claude?
Run npx skills add Ar9av/obsidian-wiki --skill wiki-dashboard -a claude-code. The skill lands in ~/.claude/skills/wiki-dashboard/SKILL.md.
What does wiki-dashboard do?
It creates dynamic views of an Obsidian vault: tables, lists, cards, indexes. The views stay current because they are built from Bases filters and formulas or from Dataview queries.
How is Bases different from Dataview?
Bases is Obsidian's native format and needs no plugin, but does less. Dataview is a community plugin with SQL-like syntax, and you want it when you need GROUP BY or computed columns.
Source text
SKILL.md · MIT
---
name: wiki-dashboard
description: >
Create dynamic, queryable Obsidian dashboard views using Bases or Dataview. Use for auto-updating tables, indexes, or structured views of vault content; not for ordinary question answering.
---
# Wiki Dashboard — Dynamic Vault Views
Two tools available: **Obsidian Bases** (native, GUI-driven, no plugin) and **Dataview** (community plugin, SQL-like, more powerful). Check which the user has and prefer Bases unless they ask for Dataview or need GROUP BY / computed columns.
## Before You Start
**Writing profile:** Before drafting or rewriting natural-language Markdown, read and apply the `Writing Profile Resolution` section in `llm-wiki/SKILL.md`. Framework schema, provenance, safety, and operation-specific requirements take precedence.
Apply `WRITING.md` preferences only to optional Markdown dashboard prose; `.base` syntax remains unchanged.
1. **Resolve config** — follow the Config Resolution Protocol in `llm-wiki/SKILL.md` (inline `@name` override → walk up CWD for `.env` → global config → prompt setup). This gives `OBSIDIAN_VAULT_PATH`.
2. Read `$OBSIDIAN_VAULT_PATH/index.md` to understand what categories and pages exist.
3. Ask the user what they want to view if not specified — folder, tag, category, date range?
4. Ask if they have Dataview installed if you're unsure which tool to use.
---
## Option A — Obsidian Bases (`.base` files)
Bases are YAML files that define live views over vault notes. Native to Obsidian 1.8+, no plugin needed.
### Official canonical schema
Top-level keys:
```yaml
filters: # Global filter applied to all views (expression strings under and/or/not)
formulas: # Named computed properties — referenced as formula.<name>
properties: # Display config per property — sets displayName for column headers
summaries: # Aggregation formulas (e.g. mean, sum)
views: # Array of view definitions (required)
```
Each item in `views:`:
```yaml
views:
- type: table # table | list | cards | map
name: "View Name" # display label
limit: 50 # optional max rows
order: # column display order (list of property/formula names)
- file.name
- note.updated
groupBy: # grouping — goes INSIDE the view, NOT at top level
property: note.tags
direction: ASC # ASC | DESC
filters: # view-specific filter (merges with global filters)
and:
- 'note.status != "done"'
summaries:
formula.myFormula: Average
```
### Filter syntax — CRITICAL
**Filters use expression strings, not typed objects.** Always wrap in `and:`, `or:`, or `not:` — a bare list causes a "may only have one of and/or/not keys" parse error.
```yaml
# CORRECT
filters:
and:
- file.inFolder("concepts")
# WRONG — typed objects (parse error)
filters:
- type: folder
folder: concepts
```
Filters support nesting:
```yaml
filters:
or:
- file.hasTag("book")
- and:
- file.inFolder("concepts")
- file.hasTag("research")
- not:
- file.hasTag("archived")
```
### Property name conventions
Different contexts use different naming — confirmed from Obsidian's auto-reformat behaviour:
| Context | Frontmatter field `tags` | File name | Formula |
|---|---|---|---|
| `properties:` keys | `note.tags` | `file.name` | `formula.<name>` |
| `order:` values | `tags` (bare) | `file.name` | `formula.<name>` |
| `groupBy.property:` | `tags` (bare) | `file.name` | — |
| `filters:` expressions | `file.hasTag(...)` / `note.tags` | `file.name` | `formula.<name>` |
| `formulas:` expressions | `note.tags`, `note.updated` | `file.name` | — |
### Basic table — folder filter
```yaml
filters:
and:
- file.inFolder("concepts")
properties:
file.name:
displayName: Page
note.tags:
displayName: Tags
note.summary:
displayName: Summary
note.updated:
displayName: Updated
views:
- type: table
name: Table
order:
- file.name
- tags
- summary
- updated
```
### Cards view — folder filter
```yaml
filters:
and:
- file.inFolder("entities")
properties:
file.name:
displayName: Entity
note.title:
displayName: Full Name
note.tags:
displayName: Tags
note.summary:
displayName: Summary
views:
- type: cards
name: Cards
order:
- file.name
- title
- tags
- summary
```
### Group by property — groupBy goes INSIDE the view
When `groupBy` is set, **omit that property from `order:`** — it becomes the group header row and adding it as a column too causes duplication.
```yaml
filters:
and:
- file.inFolder("concepts")
properties:
file.name:
displayName: Concept
note.summary:
displayName: Summary
note.updated:
displayName: Updated
views:
- type: table
name: By Domain
groupBy:
property: tags # bare property name, no note. prefix
direction: ASC
order:
- file.name # do NOT include tags here — already the group header
- summary
- updated
```
### Tag filter
```yaml
filters:
and:
- file.hasTag("machine-learning")
properties:
file.name:
displayName: Page
note.category:
displayName: Category
note.summary:
displayName: Summary
views:
- type: table
name: Table
order:
- file.name
- category
- summary
```
### Multi-filter (folder AND tag)
```yaml
filters:
and:
- file.inFolder("projects")
- file.hasTag("active")
properties:
file.name:
displayName: Project
note.summary:
displayName: Summary
note.updated:
displayName: Last Updated
views:
- type: cards
name: Cards
order:
- file.name
- summary
- updated
```
### OR filter (two folders)
```yaml
filters:
or:
- file.inFolder("concepts")
- file.inFolder("entities")
properties:
file.name:
displayName: Page
note.category:
displayName: Category
note.updated:
displayName: Updated
views:
- type: table
name: Table
order:
- file.name
- category
- updated
```
### Computed column via formulas
```yaml
filters:
and:
- file.inFolder("concepts")
formulas:
days_stale: "floor((now() - note.updated) / 86400000)"
properties:
file.name:
displayName: Page
note.updated:
displayName: Updated
formula.days_stale:
displayName: Days Stale
views:
- type: table
name: Stale
order:
- file.name
- updated
- formula.days_stale
```
### Filter expression reference
| Expression | What it does |
|---|---|
| `file.inFolder("path")` | Pages in that folder |
| `file.hasTag("tag")` | Pages with that tag (no `#` prefix) |
| `file.hasLink("Note Name")` | Pages linking to a note |
| `file.name == "note-name"` | Exact filename match |
| `file.ext == "md"` | Filter by extension |
| `note.propertyName` | Any frontmatter property |
| `formula.formulaName` | A named formula result |
| `now()` | Current timestamp in ms |
> **On Obsidian UI-generated format:** When Obsidian's GUI writes or reformats a `.base` file it may output a simplified shorthand with top-level `columns:`, `sort:`, and `view:` keys instead of the canonical schema. That format also works — Obsidian accepts both. Manually authored files should use the canonical schema above.
---
## Option B — Dataview (community plugin)
Dataview uses a SQL-like query language inside ` ```dataview ``` ` code blocks in any note. More powerful than Bases for computed columns, GROUP BY, and cross-folder queries.
### Basic table — folder
````markdown
```dataview
TABLE
tags AS "Tags",
summary AS "Summary",
file.mtime AS "Last Modified"
FROM "concepts"
SORT file.mtime DESC
```
````
### Table with clickable links (TABLE WITHOUT ID)
````markdown
```dataview
TABLE WITHOUT ID
file.link AS "Entity",
tags AS "Tags",
summary AS "Summary"
FROM "entities"
SORT file.name ASC
```
````
### GROUP BY — use `rows.` prefix after grouping
After `GROUP BY`, individual file properties must be prefixed with `rows.` — otherwise the column is empty or errors.
````markdown
```dataview
TABLE WITHOUT ID
rows.file.link AS "Concept",
rows.summary AS "Summary"
FROM "concepts"
GROUP BY tags[0] AS "Domain"
```
````
### Stale pages — use `file.mtime` for date math
Avoid `choice(updated, date(updated), file.mtime)` — mixed date formats in `updated` frontmatter cause arithmetic errors. `file.mtime` is always a valid DateTime.
````markdown
```dataview
TABLE WITHOUT ID
file.link AS "Page",
category AS "Type",
file.mtime AS "Last Modified",
(date(today) - file.mtime).days + " days" AS "Age"
FROM "concepts" OR "entities" OR "projects"
WHERE file.name != file.folder
WHERE (date(today) - file.mtime).days > 30
SORT (date(today) - file.mtime).days DESC
```
````
### Multi-folder query
````markdown
```dataview
TABLE
summary AS "Summary",
file.mtime AS "Last Modified"
FROM "projects"
WHERE file.name != file.folder
SORT file.mtime DESC
```
````
### Dataview reference
| Clause | Usage |
|---|---|
| `FROM "folder"` | All notes in folder |
| `FROM #tag` | All notes with tag |
| `FROM "a" OR "b"` | Union of two folders |
| `WHERE file.name != file.folder` | Exclude folder index pages |
| `GROUP BY field AS "Label"` | Group rows — use `rows.` for properties after this |
| `SORT field DESC` | Sort direction |
| `file.link` | Clickable wikilink |
| `file.mtime` | Last modified time (always valid DateTime) |
| `(date(today) - file.mtime).days` | Days since last modification |
---
## Step 3: Write the File
**Bases:** Target path `$OBSIDIAN_VAULT_PATH/_meta/<dashboard-name>.base`
**Dataview:** Write queries directly into any `.md` note. A dedicated dashboard note at `$OBSIDIAN_VAULT_PATH/_meta/dashboard.md` works well for multi-section views.
Slug examples:
- "All concepts" → `_meta/concepts-index.base`
- "Recent ingests" → `_meta/recent-ingests.base`
- "Project overview" → `_meta/projects-overview.base`
- "Stale pages" → `_meta/stale-pages.base`
- "Full dashboard" → `_meta/dashboard.md`
Create `_meta/` if it doesn't exist yet.
## Step 4: Embed Bases (optional)
To embed a `.base` inside a note:
```markdown
## Entities
![[_meta/entities-tracker.base]]
```
Ask before modifying an existing note.
## Step 5: Update Tracking
Append to `$OBSIDIAN_VAULT_PATH/log.md`:
```
- [TIMESTAMP] WIKI_DASHBOARD name="<slug>" tool=bases|dataview view=<type> filter="<description>"
```
No manifest or index update needed — dashboards are live queries, not static pages.
## Common Dashboard Recipes
| Dashboard | Best tool | What it shows |
|---|---|---|
| **Content index** | Bases or Dataview | All pages grouped by category, sorted by updated |
| **Entity tracker** | Bases (cards) | Entity pages as a visual card gallery |
| **Concepts by domain** | Dataview | Concepts grouped by first tag using GROUP BY |
| **Ingestion log** | Either | Pages sorted by `created` date |
| **Stale content** | Dataview | Pages not touched in 30+ days with day count |
| **Project overview** | Either | Project pages with last-sync date |
| **Research tracker** | Dataview | Synthesis pages tagged `research` |
## Quality Checklist
- [ ] Bases: filters use expression strings under `and:`/`or:`/`not:`, never typed objects
- [ ] Bases: `groupBy` goes inside the view definition — not as a top-level key
- [ ] Bases: column headers set via `properties: <name>: displayName: "..."`, not `columns: [{title}]`
- [ ] Bases: `formulas:` used for computed columns, referenced as `formula.<name>` in order/properties
- [ ] Dataview: GROUP BY queries use `rows.property` not bare `property`
- [ ] Dataview: date arithmetic uses `file.mtime`, not `choice(updated, ...)`
- [ ] File written to `_meta/` with a descriptive slug
- [ ] `log.md` updated
- [ ] User told how to embed Bases (`![[_meta/<name>.base]]`) or open the dashboard note
## QMD Refresh After Vault Writes
QMD is a search index, not the source of truth. If `$QMD_WIKI_COLLECTION` is empty or unset, skip this step. Run it only after this skill has written or rewritten vault markdown. If QMD refresh fails, do not roll back the vault changes; report the QMD status separately.
Use `$QMD_CLI` if set; otherwise use `qmd`.
```bash
${QMD_CLI:-qmd} update
```
If the output says vectors are needed or embeddings may be stale, run:
```bash
${QMD_CLI:-qmd} embed
```
Verify the collection with either:
```bash
${QMD_CLI:-qmd} ls "$QMD_WIKI_COLLECTION"
```
or, when a specific page path is known:
```bash
${QMD_CLI:-qmd} get "qmd://$QMD_WIKI_COLLECTION/<page>.md" -l 5
```
Record one of:
- `QMD refreshed: update + embed + verified`
- `QMD refreshed: update only + verified`
- `QMD skipped: QMD_WIKI_COLLECTION unset`
- `QMD skipped: qmd CLI unavailable`
- `QMD failed: <short error summary>`
© Ar9av · MIT · Ar9av/obsidian-wiki