Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Customising templates and frontmatter

When cdno scaffolds a note it fills a template. Every note type has a built-in template, and you can override any of them per-vault — to change the structure, the default sections, or the frontmatter fields. You can also require extra frontmatter fields so cdno lint keeps your notes consistent. This tutorial walks through both, hands-on.

What’s covered here is the shipped behaviour. Both kinds of config variable now resolve in custom templates: static [variables] and interactive [variables.prompt] (gathered from a TTY prompt or a --var name=value flag).

Where templates live

Templates live in .cuaderno/templates/, one Markdown file per note type (e.g. project.md, evidence.md). cdno resolves the effective template at creation time:

  1. a custom variant file — <type>-<variant>.md (tracking uses this: the variant is the activity slug, e.g. tracking-gym.md), then
  2. a custom type file — <type>.md, then
  3. the built-in variant default, then
  4. the built-in type default.

So a custom file in .cuaderno/templates/ always wins over the built-in. No activity-specific variants ship built-in (tier 3 is empty today) — tracking variants are entirely yours to add; see Tracking variants below.

cdno init writes two starter templates — .cuaderno/templates/daily.md, and concept.md for the concept custom type it declares. Every other built-in type uses its built-in default until you add a file for it. The quickest way to get an editable copy of a built-in is cdno templates eject <type> — e.g. cdno templates eject project writes .cuaderno/templates/project.md matching the built-in, ready to edit. You can also just create the file yourself, as the next section shows.

Customise a template

Say you want every project to start with a ## Risks section. Eject the built-in as a starting point:

cdno templates eject project        # writes .cuaderno/templates/project.md

That writes the full built-in template. Insert a ## Risks section (you can reference any of the {{placeholders}} that cdno templates vars project lists) and leave the rest as-is, so .cuaderno/templates/project.md reads:

---
type: project
context: {{context}}
status: {{status}}
created: {{created}}
core_question: {{core_question}}
closed: null
---

# {{title}}

## Current State
New project. No work done yet.

## Risks

## Next Actions
- [ ] Define first concrete step (light)

## Waiting On
(nothing yet)

## Milestones
- [ ] First milestone — target: TBD

## Links
- Portfolio: (none yet)

Now create a project:

cdno project create --title "Surrogate model" --context work

The new projects/surrogate-model.md follows your template — including the ## Risks section:

---
type: project
context: work
status: active
created: 2026-06-30
core_question: null
closed: null
---

# Surrogate model

## Current State
New project. No work done yet.

## Risks

## Next Actions
- [ ] Define first concrete step (light)

## Waiting On
(nothing yet)

## Milestones
- [ ] First milestone — target: TBD

## Links
- Portfolio: (none yet)

cdno templates eject <type> is the recommended way to get an editable base — it matches the built-in at the moment you eject. (You could instead hand-write the file, or shape it from a note cdno already created, but eject saves the guesswork.) If a later release adds a frontmatter key to the built-in, your copy does not gain it by itself: cdno lint and cdno templates list say so, and cdno templates sync <type> adds it without touching your edits.

Editing a template only affects notes created afterwards — existing notes are untouched. (And cdno normalise only reorders frontmatter keys; it won’t add a new section like ## Risks to old notes.)

closed: in a customised project template. The built-in project template ends its frontmatter with closed: null, the date a project is completed or dropped. A .cuaderno/templates/project.md you ejected before that key existed does not gain it by itself, and nothing breaks if it never does: closing a project writes the key whenever it is missing. cdno lint and cdno templates list say when your template lacks it, and cdno templates sync project adds it next to its built-in neighbour (after core_question: when you kept that) without touching anything else. The same command covers any key a future release adds to a built-in template.

Tracking variants

tracking is the one type whose template is chosen per activity. cdno track <activity> slugifies the activity and looks for .cuaderno/templates/tracking-<activity>.md, falling back to the generic tracking template when there’s none. So a custom .cuaderno/templates/tracking-gym.md gives cdno track gym a bespoke layout without touching cdno track swim, and .cuaderno/templates/tracking.md overrides the generic fallback for everything else.

Only the neutral generic template ships built-in — no activity-specific variants are baked into the product. Ready-made gym, body, and swim variants (exercise table, body-metrics table, swim-set table) live in the repo under examples/templates/tracking/; copy one to .cuaderno/templates/tracking-<activity>.md to use it, or start your own from it.

Template variables

Templates use {{placeholder}} markers. cdno substitutes the values the note’s creation command supplies. Two rules to know:

  • An omitted optional value renders as null (e.g. core_question: null above when you don’t pass --question).
  • An unknown placeholder is left verbatim — {{nope}} stays as the literal text {{nope}} in the note. So a template should only use the placeholders its note type actually provides.

Each type provides these:

Note typeAvailable {{placeholders}}
dailydate, heading, weekday, day_name, week
weeklyweek, week_num, year, date_start, date_end
monthlymonth, month_name, year, date_start, date_end, weeks
projecttitle, context, status, created, core_question
actiontitle, slug, project, energy, status, created, due, completed, milestone, criteria, blocker, tags
portfolioquestion, project, created
evidencesource, origin, portfolio, content, created
stewardshipname, context
trackingstewardship, activity, activity_title, routine, content, date, date_long
questionquestion, domain, created, updated
commitmenttitle, context, status, due, project, stewardship, created, completed
inboxbody, created

You can use any subset, in any order, and add as much static Markdown around them as you like.

Daily specifics. weekday and day_name are aliases for the same value — the weekday name (e.g. Sunday) — so use whichever reads better. week is the ISO-week label YYYY-Www (e.g. 2026-W27), matching the weekly note for that date, so [[{{week}}]] in a daily template links straight to it.

Discover them from the CLI. cdno templates vars <type> lists exactly this table for a type — the complete set its create path supplies — and folds in any [variables] / [variables.prompt] names your config adds, classified by source. For example cdno templates vars tracking. See the templates reference.

Static config variables

Beyond the per-type placeholders above, a custom template can reference vault-wide static variables you define under [variables] in .cuaderno/config.toml. These resolve on every note type. For example:

# .cuaderno/config.toml
[variables]
author = "A. Researcher"
institution = "University of Examples"

A custom template can then use {{author}} / {{institution}} and they’ll be substituted at creation. Precedence: a per-type (contextual) placeholder of the same name always wins over a config variable, so config vars only fill names the note type doesn’t already supply.

Prompted variables

A static variable is the same on every note. When you want a value that changes per note — a ticket id, a collaborator, a meeting code — declare it under [variables.prompt], where the value is the prompt message:

# .cuaderno/config.toml
[variables.prompt]
ticket = "Ticket reference?"

Reference it in a custom template like any other placeholder (e.g. ticket: {{ticket}} in the project frontmatter). When you create a note whose effective template uses a prompted variable, cdno gets the value one of three ways:

  • --var name=value on the command (repeatable), e.g. cdno project create --title T --context work --var ticket=ABC-123;
  • otherwise, in an interactive TTY, cdno asks (“Ticket reference?”) and shows the value in the confirm preview before writing;
  • otherwise (non-interactive, no --var) it errors rather than writing a note with a literal {{ticket}}:
Error: missing value for template variable 'ticket' (pass `--var ticket=value`, set a default under
[variables] in .cuaderno/config.toml, or run interactively in a TTY)

--var is available on every note-creating command: project create, question create, stewardship create, commit create, portfolio create, file, track, action add --note, and action promote.

A few rules worth knowing:

  • A prompted name that also has a static [variables] default is satisfied by that default — you’re not asked, and it won’t error. (The static default wins by precedence, so --var can’t override it; remove the default if you want to be prompted.)
  • A [variables.prompt] entry whose {{name}} your template doesn’t actually use is ignored.
  • The same precedence applies: a per-type placeholder of the same name wins over a prompted variable.
  • --var only applies to templated notes. cdno file --attach (the attachment stub) and a plain action add (no --note) aren’t templated, so --var is ignored there.
  • The implicit-write paths — daily (log), weekly, and inbox (capture) notes — don’t gather prompted values, and neither do MCP-driven creations (there’s no --var over MCP). A [variables.prompt] placeholder in one of those templates fails at creation (an UnresolvedPrompts error) instead of being asked for; give it a static [variables] default instead.

Frontmatter field order and normalise

Your template also defines the canonical order of frontmatter keys for that type. Notes cdno creates are already in that order; for hand-authored or migrated notes, cdno normalise reorders their frontmatter to match the template (--check reports without writing). So if you reorder the keys in project.md, a later cdno normalise brings older project notes into line.

Require extra frontmatter fields

Beyond the built-in required fields, you can demand vault-specific ones per type with a [schemas.<type>] section. For example, to require every project to name an owner, add to .cuaderno/config.toml:

[schemas.project]
extra_required = ["owner"]

This is enforced by cdno lint, which now errors on any project missing the field (a missing key, or one whose value is null, fails):

cdno lint
# [error] projects/surrogate-model.md: missing required field `owner` for note type `project`
# Error: found 1 error(s), 0 warning(s)

lint exits non-zero on errors, so this is a good gate to run in a pre-commit hook or CI over a git-tracked vault.

Satisfy the requirement going forward

Add the field to your template so new notes carry it. Give it a non-null default you can edit per note (an empty key — owner: — is YAML null and still fails the lint; use a placeholder value):

---
type: project
context: {{context}}
status: {{status}}
created: {{created}}
core_question: {{core_question}}
closed: null
owner: unassigned
---

New projects are now born with owner: unassigned (edit it as needed) and pass the lint. Existing notes aren’t changed retroactively — fix them by adding the field, then re-run cdno lint until it’s clean.

Required fields are about presence, not value: any non-null value satisfies the check. Combine extra_required with a template default and the occasional cdno lint and your vault stays uniform without any per-note ceremony.

Give a field a type

extra_required only checks that a key is present. When you want cdno lint to also check the value — that meds is a boolean, mood is one of a fixed set, since is a real date — declare a typed field instead, under [schemas.<type>.fields.<name>]:

[schemas.daily.fields.meds]
type = "bool"
default = false

[schemas.daily.fields.mood]
type = "string"
values = ["low", "ok", "good"]

Now a daily note whose meds: isn’t a boolean, or whose mood: isn’t one of the three allowed values, gets a cdno lint warning. Typed fields are also recognised by cdno templates, so a custom template referencing {{meds}} no longer warns that it “renders literally”.

A typed field’s default is populated at create, too: add meds: {{meds}} to your custom daily.md and every new daily note is scaffolded with meds: false — the declared default — rather than a literal {{meds}}. A field with no default renders null. (As with any placeholder, the field only appears in a note if the template references it; and if the note’s create path already supplies that name, or a [variables] static var does, that value wins over the default.) See Typed schema fields in the configuration reference for the full grammar and its limits.

Edit templates from the CLI

cdno templates covers the whole loop without leaving the terminal.

cdno templates list                         # every type, which template is in effect, and where
cdno templates show project                 # the effective content, verbatim
cdno templates eject project                # copy the built-in default into .cuaderno/templates/
cdno templates save --note-type project     # write it: --file, --file - for stdin, or $EDITOR
cdno templates sync --all                   # add keys a release added to the built-ins, keep your edits
cdno templates new --note-type people       # scaffold a starter for a custom type

list names the source in effect for each type — builtin_default, custom_base, a variant, or none for a custom type with no template yet. save on a built-in creates the override for you, so it needs no prior eject, and show is byte-verbatim, which means the round trip holds:

cdno templates eject project
cdno templates show project | diff - .cuaderno/templates/project.md   # no output

See the templates reference for every flag.

See also