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 just one starter template — .cuaderno/templates/daily.md. Every other 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}}
---

# {{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
---

# 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 always matches the current built-in. (You could instead hand-write the file, or shape it from a note cdno already created, but eject saves the guesswork.)

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.)

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}}
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 the desktop Templates editor, 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 in the desktop app

Everything above works from the CLI, but the desktop app also has a Templates editor that does the same job without a terminal — press ⌘, and pick Templates from the settings rail.

You get a chip for every note type — the built-ins plus any custom types you declared under [note_types.<name>]. Selecting one names its source in the editor’s header:

  • Built-in — the type is using its built-in default (no override yet).
  • Custom — a custom override exists in .cuaderno/templates/.
  • No template — a custom type that has no template file yet.

The chips themselves flag only the last two, since most types sit on their built-in default.

Select a type to see its effective template in the editor. Edit the text and press Save: the app writes .cuaderno/templates/<type>.md. For a type currently on the built-in default, that first save creates the custom override — the same edit-and-save model as cdno templates eject followed by an edit, but in one step. A custom type showing No template offers Create, which scaffolds a starter from the type’s declared required fields.

The side panel lists the placeholders you can use, grouped by where their value comes from — supplied keys the create path fills, a custom type’s own schema fields, and any config variables or prompted variables. If you type a {{token}} that isn’t in that set, the editor shows a calm inline notice so you can catch a typo before it renders literally — but it never blocks you from saving. An edit made outside the app (in your editor, or by another tool) refreshes the view automatically.

See also