---
name: metadata-menu
description: How to write frontmatter/Properties that conform to Metadata Menu (mdelobelle's Obsidian plugin) — the fileClass + field-definition system that types and validates note metadata. Load BEFORE writing or editing any note's frontmatter, authoring or extending a fileClass, choosing a field type, or driving MetadataMenu.api. Covers all 22 field types with their exact YAML value shapes, fileClass mapping + inheritance, the indexedPath model, and the programmatic API (postNamedFieldsValues, insertMissingFields, fileFields). Triggers on: Metadata Menu, MDM, fileClass, field definition, frontmatter, YAML properties, Property/Properties, "write the front matter", insert missing fields, postValues/postNamedFieldsValues, indexedPath, Select/Multi/Cycle/Lookup/Formula fields, Obsidian metadata authoring.
---

# Metadata Menu — how to write frontmatter that the vault will honor

Metadata Menu (MDM) turns loose Obsidian frontmatter into a **governed, typed,
validated** system. A field isn't just `key: value` — it has a **definition**
(a type + options) that lives in a **fileClass**, and a note's frontmatter is
expected to *conform* to that definition. Conform, and everything downstream
works: the field modal, autocompletion, the table view, validation, the API.
Write it blind and you get valid-looking YAML that MDM flags as invalid.

**The one-line model:** *a fileClass is a schema; a note's frontmatter is an
instance of it; the field type is the contract on each value.*

## THE IRON RULE — read the contract before you write the instance

**Never guess a field's type or allowed values.** They are defined somewhere
concrete, and that definition is the truth:

1. **The fileClass file** (a note in the fileClass folder) — its frontmatter
   `fields:` array is the authoritative list of `{name, type, options}`.
2. **At runtime**, `MetadataMenu.api.namedFileFields(file)` returns every field's
   `type`, `options`, `isValid`, and `sourceType` — the live contract *and*
   whether the current value satisfies it.

Before authoring a note's frontmatter: know which fileClass(es) map to it, read
their fields, and write values that match each field's **type**. For a
`Select`/`Multi`/`Cycle`, the value MUST be one of the defined options. For a
`File`, it must be a valid `[[link]]`. Guessing is how you produce `isValid:false`.

## Two places a field can live (both are "the field")

- **Frontmatter (a "Property" in Obsidian):** `name: value` inside the `---` block.
- **Inline (Dataview notation, needs Dataview):** `name:: value` on its own line,
  or embedded `... (name:: value) ...` / `... [name:: value] ...`.

Frontmatter is the default and the only place for the structural types (`YAML`,
`Object`, `ObjectList`). The plugin's "Scope" setting decides whether MDM also
indexes inline fields.

## THE 22 FIELD TYPES — and the exact YAML each one accepts

When you write frontmatter, the value shape must match the field's type.

| Type | Accepts | Frontmatter value shape |
|---|---|---|
| **Input** | any string (default type) | `title: Anything you like` — `{{templates}}` help the *modal*, not the value |
| **Boolean** | true / false (anything not `true` → `false`) | `done: true` |
| **Number** | int or float (opt. step/min/max) | `count: 42` · `ratio: 0.75` |
| **Select** | ONE value from a defined list | `status: Active` — must be a defined option |
| **Multi** | MANY values from a list | inline: `tags: [a, b]` · or indented list (below) |
| **Cycle** | one value, cycles through a list | `phase: Draft` — one option, advanced by command |
| **File** | ONE internal link (Dataview query source) | `parent: "[[Some Note]]"` |
| **MultiFile** | many internal links | `refs: ["[[A]]", "[[B]]"]` or indented list |
| **Media** | one link to a media file | `cover: "[[img.png]]"` |
| **MultiMedia** | many media links | list of media links |
| **Date** | a date (moment.js format; opt. as `[[link]]`) | `due: 2026-07-16` or `due: "[[2026-07-16]]"` |
| **DateTime** | date + time | `at: 2026-07-16T14:30` |
| **Time** | time only | `alarm: "14:30"` |
| **Lookup** | auto-filled from a Dataview query (persistent) | *plugin-written* — don't hand-author |
| **Formula** | JS result from other note fields | *plugin-written* — don't hand-author |
| **Canvas** / **CanvasGroup** / **CanvasGroupLink** | auto-filled from canvas connections | *plugin-written* — don't hand-author |
| **JSON** | a JSON object (free properties) | serialized "JSON in YAML" |
| **YAML** | a YAML object (free properties) *frontmatter-only* | a nested YAML block |
| **Object** | a parent holding nested child fields *frontmatter-only* | indented YAML (see nesting) |
| **ObjectList** | a list of objects (repeating group) *frontmatter-only* | YAML list of indented blocks |

**Rules that bite:**
- **Type is immutable once saved.** To change it, remove the field from the
  fileClass and recreate it — otherwise the index goes inconsistent.
- **`Lookup`, `Formula`, `Canvas*` are plugin-maintained.** Don't write their
  values by hand; MDM computes and overwrites them.
- **`YAML`, `Object`, `ObjectList` are frontmatter-only.**
- Field **name is case-sensitive** and **unique per level per fileClass**.

## Multi-value display: inline array vs indented list

A `Multi`/`MultiFile` field's "Frontmatter List display type" decides how it's
written. **Both are valid** — match the field's setting for clean round-trips:

```yaml
# inline array
tags: [pkm, music]

# indented list (equivalent)
tags:
  - pkm
  - music
```

## Select / Multi / Cycle options — the index-map shape

In a **fileClass** the options are an *ordered map* of index → label:

```yaml
- name: grade
  type: Select
  options:
    "0": A
    "1": B
    "2": C
  id: aB3dEf
  path: ""
```

Options can also come from **a note path** (each line = an option) or **a JS
function** returning `string[]` (`dv` and `current` are in scope). In a *note's*
frontmatter you write only the chosen **label** (`grade: A`), never the index.

## Nesting — Object / ObjectList (the `____` and `[i]` model)

An `Object` field is a parent; children set it as their `parent`. An `ObjectList`
is a repeating group. In the note it's just indented YAML:

```yaml
Employees:                       # ObjectList
  - Name: John Doe
    Role: CFO
    Contact Info:                # Object nested inside the list item
      e-mail: john@acme.ob
      phone number: "123"
  - Name: Ann Martin
    Role: CEO
```

**indexedPath** identifies one field *instance*: parent ids joined by `____`, with
`[position]` after an ObjectList's id, ending in the field's id — e.g.
`dx8Mth[0]____Y0dsfZ____hRlSsW`. You need it for `postValues`; the **named** API
lets you skip it.

## FILECLASS — the schema file

A fileClass is a note in the fileClass folder (path set in settings, trailing
slash). **Filename = class name.** Sub-folders allowed: `fileClass: sub/name`.
Its frontmatter holds settings + the `fields:` array; its body is free text.

**Mapping a note to a fileClass** (priority order, highest first):
1. `fileClass:` in the note's frontmatter (single or list) ← the explicit way
2. **tag** match (`mapWithTag: true`, or a `tagNames` alias)
3. **path** match (`filesPaths`)
4. **bookmark group** match
5. **fileClassQuery** (a Dataview query in settings)
6. **global fileClass** (settings)
7. **preset fields** (settings — vault-wide, lowest priority)

```yaml
---
fileClass: music            # one class
---
---
fileClass:                  # multiple classes on one note
  - company
  - pkm
---
```

**Inheritance:** `extends: parentClass` pulls in the parent's fields. Redefining a
field of the same name **overrides** it; `excludes: [grade]` drops an inherited
field. Settings worth knowing: `extends`, `excludes`, `mapWithTag`, `tagNames`,
`filesPaths`, `bookmarkGroups`, `buttonIcon` (a lucide.dev name),
`maxRecordsPerPage`, `version` (system-managed — don't touch).

## THE API — `MetadataMenu.api` (write frontmatter programmatically)

Ground truth is `src/MetadataMenuApi.ts`, not the prose docs — where they differ,
the TS wins (the docs still list a dropped `after?` param on `postValues`).

**The go-to writer — by field NAME (no indexedPath needed):**
```typescript
await MetadataMenu.api.postNamedFieldsValues(
  fileOrPath,                                  // TFile | string
  [{ name: "status", payload: { value: "Active" } },
   { name: "tags",   payload: { value: "[pkm, music]" } }],
  lineNumber?,        // omit → lands in frontmatter (created if missing)
  asList?,            // prepend "- "  (default false)
  asBlockquote?       // prepend "> "  (default false)
)
```
`value` is ALWAYS a string — MDM parses it per the field's type.

**By indexedPath (needed for ObjectList instances):**
`postValues(fileOrPath, [{indexedPath, payload:{value}}], lineNumber?, asList?, asBlockquote?)`

**Scaffold a note from its fileClass — insert every missing field:**
```typescript
await MetadataMenu.api.insertMissingFields(
  fileOrPath, lineNumber, asList, asBlockquote, fileClassName? // omit → all mapped classes
)
```

**Introspect + validate (do this BEFORE writing to know the contract):**
- `namedFileFields(file)` → per field: `value`, `type`, `options`, `isValid`,
  `sourceType`, `id`, `fileClassName`. **`isValid` is your conformance check.**
- `fileFields(file)` → same, keyed by raw indexedPath.
- `getValuesForIndexedPath(file, indexedPath)` → one value; `getValues(file, name)`
  → all values for a name (deprecated, still works).
- `fieldModifier(dv, p, name, {options:{alwaysOn, showAddField, inFrontmatter}})`
  → an editable control inside a `dataviewjs` table.

## Codeblock — render a fileClass table anywhere

````
```mdm
fileClass: <name>       (mandatory)
view: <saved view>      (optional)
files per page: <n>     (optional)
start: <n>              (optional)
showAddField: <bool>    (optional, default false)
```
````

## AUTHORING PROCEDURE (the "properly write a front matter" drill)

1. **Identify the fileClass(es)** for the note (frontmatter `fileClass:`, or the
   tag/path/query rules, or the global fileClass).
2. **Read the contract** — the fileClass's `fields:`, or `namedFileFields(file)`
   at runtime. Note the plugin-maintained types (Lookup/Formula/Canvas*) and
   leave those to MDM.
3. **Write values that match each type** — Select/Multi from the options; File/
   Media as `[[links]]`; Date in the field's moment format; nesting as indented
   YAML for Object/ObjectList.
4. **Prefer the tools over hand-YAML** when a plugin is driving it:
   `insertMissingFields` to scaffold, `postNamedFieldsValues` to set values.
5. **Verify** with `isValid` from `namedFileFields` — don't declare it done on
   "the YAML parses." Valid YAML ≠ conformant field.

## GOTCHAS — the short list

- Valid YAML is not a valid field. Conformance is `isValid`, per the type.
- Don't hand-write `Lookup`/`Formula`/`Canvas*` values — they're computed.
- `Object`/`ObjectList`/`YAML` are frontmatter-only.
- Type can't change after save; name is case-sensitive + unique-per-level.
- `id`s in a fileClass are plugin-generated and unique — don't invent or reuse
  them by hand; let the UI/API create fields so ids stay consistent.
- FileClass beats plugin-settings preset fields when both define the same name.

## SOURCE OF TRUTH

- Docs: `mdelobelle.github.io/metadatamenu`
- API: `github.com/mdelobelle/metadatamenu` → `src/MetadataMenuApi.ts`

When the machinery matters, re-read the source — this skill is the map, the
plugin is the territory.
