SKILL.md
$27
Top-level keys:
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::
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.
# CORRECT
filters:
and:
- file.inFolder("concepts")
# WRONG — typed objects (parse error)
filters:
- type: folder
folder: concepts
Filters support nesting:
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
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
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.
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
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)
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)
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
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
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)
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.
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.
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
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:
## 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:
groupBygoes inside the view definition — not as a top-level key
- Bases: column headers set via
properties: <name>: displayName: "...", notcolumns: [{title}]
- Bases:
formulas:used for computed columns, referenced asformula.<name>in order/properties
- Dataview: GROUP BY queries use
rows.propertynot bareproperty
- Dataview: date arithmetic uses
file.mtime, notchoice(updated, ...)
- File written to
_meta/with a descriptive slug
log.mdupdated
- 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.
${QMD_CLI:-qmd} update
If the output says vectors are needed or embeddings may be stale, run:
${QMD_CLI:-qmd} embed
Verify the collection with either:
${QMD_CLI:-qmd} ls "$QMD_WIKI_COLLECTION"
or, when a specific page path is known:
${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>