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
Recommended by author
This prompt takes no variables — just pick a model and run.
# 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.<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>`Running prompts needs a free account.
Sign in and we'll stream the response from Claude Opus 4.7 right here — no config needed for the platform models.
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
# 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.<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>`