--- name: wiki-dashboard description: > Create dynamic, queryable dashboard views of the Obsidian vault using Obsidian Bases or Dataview. Use this skill when the user says "create a dashboard", "vault dashboard", "show all X as a table", "dynamic view", "query my vault", "build a content index", "show me all concepts/entities/projects", or wants a structured, auto-updating view of their wiki content. Bases is native to Obsidian 1.8+ (no plugin needed). Dataview requires the community plugin. --- # 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 1. **Resolve config** — follow the Config Resolution Protocol in `llm-wiki/SKILL.md` (walk up CWD for `.env` → `~/.obsidian-wiki/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. 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.` | | `order:` values | `tags` (bare) | `file.name` | `formula.` | | `groupBy.property:` | `tags` (bare) | `file.name` | — | | `filters:` expressions | `file.hasTag(...)` / `note.tags` | `file.name` | `formula.` | | `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/.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="" tool=bases|dataview view= filter="" ``` 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: : displayName: "..."`, not `columns: [{title}]` - [ ] Bases: `formulas:` used for computed columns, referenced as `formula.` 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/.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/.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: `