README file from
GithubMetadata Validator
Describe what a note's properties should look like in a manifest.md, and the plugin checks every matching note, fills in what it can, and keeps the properties in order. Rules add the conditional part, for example "when the status is done, set the end date".
Where manifests live
Manifests sit in a schemas folder (set in the plugin settings) and inherit down the tree. A note gets the closest manifest whose target matches it.
vault/
└── schemas/
├── manifest.md base schema
├── rules.md rules for every manifest below
├── sources/
│ ├── manifest.md extends base
│ └── books/
│ └── manifest.md extends sources
└── projects/
└── manifest.md extends base
A complete example
One manifest for project notes. It matches notes tagged #project, describes their properties, keeps them in a fixed order, and has three rules.
---
name: Project
description: A finite piece of work with one result.
target:
query: "#project"
enforce_folder: projects
fields:
tags:
type: multiselect
required: true
sort: alphabetical
status:
type: select
required: true
default: inbox
options:
- { value: inbox, label: Inbox }
- { value: wip, label: In progress }
- { value: done, label: Done }
priority:
type: select
default: normal
options: [{ value: high }, { value: normal }, { value: low }]
category:
type: multilink
source:
query: "#category"
start:
type: date
end:
type: date
created:
type: date
hidden: true
icon:
type: text
hidden: true
fixed: 🗂️
rules:
- name: start on wip
when: "status=wip AND start="
then:
set: { start: "{{today}}" }
- name: end on done
when: "status=done AND end="
then:
set: { end: "{{today}}" }
- name: created stamp
when: "created="
then:
set: { created: "{{now}}" }
formatting:
property_order: [tags, status, priority, category, start, end, created, icon]
---
This runs on save, on open, and when you start the vault auto-fix from the sidebar. Missing properties get their default, icon is always the fixed value, the rules fill in the empty dates and the creation time, and the properties are reordered. Anything the plugin cannot fix shows up in the sidebar and as an icon next to the property.
Manifest keys
| Key | Meaning |
|---|---|
name, description |
shown in the sidebar and in the properties modal |
target.query |
which notes this manifest applies to, see the query language below |
enforce_folder |
notes matching this manifest are moved into this folder |
extends |
path of the parent manifest, when it is not the parent folder |
exclude |
names of inherited fields or rules to drop |
priority |
when several manifests match, the higher priority wins |
fields |
the properties, see below |
rules |
the rules, see below |
formatting.property_order |
order of properties in the note |
Field keys
Types are text, number, select, multiselect, list, date, link, multilink, boolean and url.
| Key | Meaning |
|---|---|
required |
the property must be present |
default |
inserted when the property is empty |
fixed |
always this value |
options |
allowed values, a list of { value, label, description }, or { source: ... } for a computed list |
strict: false |
values outside options are left alone |
source |
for links, which notes are allowed. query, folder, tag or js |
validate_exists: false |
do not require the linked note to exist |
sort |
alphabetical or alphabetical-desc for lists |
min, max |
range for numbers |
format |
date format, for example YYYY-MM-DD |
label, description, hidden |
how the property appears in the editing modal |
validate.js |
custom check, return true or an error message |
Query language
Used in target.query, in source.query and in rule conditions.
| Term | Matches when |
|---|---|
#project |
the note has this tag or a child tag |
projects/ |
the note is in this folder |
status=done |
the property equals the value, or a list contains it |
end= |
the property is empty |
rating>=8, end<2026-01-01 |
numbers and dates compare as such |
-term, AND, OR, ( ) |
negation, both, either, grouping |
Rules
A rule says what the properties should look like when a condition holds. It runs on the note's current state, so it behaves the same on save, on open and in the vault auto-fix, and it does nothing when the note is already correct.
rules:
- name: end on done
description: Done work gets its end date.
when: "status=done AND end="
then:
set: { end: "{{today}}" }
name and description are optional. The properties modal lists the rules under the schema and shows the description on hover.
when is a query string, or one of these forms, and can be nested.
when: { project: { when: "status=done" } } # a linked note in `project` matches
when: { tasks: {} } # `tasks` has at least one value
when: { and: ["status=done", { not: ["end="] }] } # also `or`
when: { js: "return fm.status === 'done'" }
then holds verbs that run in the order written.
| Verb | Does |
|---|---|
set: { end: value } |
write the value, null clears |
add: { tags: value } |
add to a list, no duplicates |
remove: { tags: value } |
remove from a list, * is a wildcard |
js: "..." |
run code with the note's fm |
A value is a literal, a template or a selection. A selection { team: { when: "status=left" } } means the links in team whose note matches, and { team: {} } means all of them.
Templates use {{today}}, {{now}}, {{file.name}} or any property. {{project>category}} reads category from the notes linked in project. Filters are name (link to note name), snake, kebab, lower, upper, trim, replace:"a","b", join:", " and date:"YYYY-MM-DD".
Common rules
Dates that follow the status.
- when: "status=wip AND start="
then:
set: { start: "{{today}}" }
- when: "status=dropped"
then:
set: { end: null, priority: low }
Tags that mirror a property. The first rule removes every status/* tag, the others add the right one.
- then:
remove: { tags: "status/*" }
- when: "status=wip"
then:
add: { tags: status/wip }
- when: "status=done"
then:
add: { tags: status/done }
Links that belong in another property. When a person in team has left, their link moves to former_team.
- then:
add:
former_team: { team: { when: "status=left" } }
remove:
team: { when: "status=left" }
A reading list that sorts itself. Books move between to_read, reading and finished by their own status.
- then:
add:
reading: { to_read: { when: "status=reading" }, finished: { when: "status=reading" } }
finished: { to_read: { when: "status=done" }, reading: { when: "status=done" } }
remove:
to_read: { when: "-status=to_read" }
reading: { when: "-status=reading" }
finished: { when: "-status=done" }
A task that follows its project. The values are copied in both directions, so a category removed from the project leaves the task too. A task without a project keeps its own.
- then:
set: { category: "{{project>category}}", area: "{{project>area}}" }
State taken from a linked note. A task closes when its project closes, and a project closes when all its tasks are closed.
- when: { project: { when: "status=done" } }
then:
set: { status: done }
- when:
and:
- tasks: {}
- not:
- tasks: { when: "-status=done" }
then:
set: { status: done }
Overdue work.
- when: "end<{{today}} AND -status=done"
then:
add: { tags: overdue }
- when: { or: ["end>={{today}}", "status=done", "end="] }
then:
remove: { tags: overdue }
Where rules live
Rules go in a manifest, or in a rules.md file anywhere in the schemas folder. A rules.md applies to every manifest in its folder and below. Rules from rules.md files run first, then the manifest chain from the root ancestor down. A rule with the same name replaces the earlier one.
When a note changes, the notes whose rules read it through a link are checked again, so a task follows its project right away. This can be turned off in the settings.