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=valueflag).
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:
- a custom variant file —
<type>-<variant>.md(trackinguses this: the variant is the activity slug, e.g.tracking-gym.md), then - a custom type file —
<type>.md, then - the built-in variant default, then
- 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 initwrites 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 iscdno templates eject <type>— e.g.cdno templates eject projectwrites.cuaderno/templates/project.mdmatching 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 normaliseonly reorders frontmatter keys; it won’t add a new section like## Risksto 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: nullabove 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 type | Available {{placeholders}} |
|---|---|
daily | date, heading, weekday, day_name, week |
weekly | week, week_num, year, date_start, date_end |
monthly | month, month_name, year, date_start, date_end, weeks |
project | title, context, status, created, core_question |
action | title, slug, project, energy, status, created, due, completed, milestone, criteria, blocker, tags |
portfolio | question, project, created |
evidence | source, origin, portfolio, content, created |
stewardship | name, context |
tracking | stewardship, activity, activity_title, routine, content, date, date_long |
question | question, domain, created, updated |
commitment | title, context, status, due, project, stewardship, created, completed |
inbox | body, created |
You can use any subset, in any order, and add as much static Markdown around them as you like.
Daily specifics.
weekdayandday_nameare aliases for the same value — the weekday name (e.g.Sunday) — so use whichever reads better.weekis the ISO-week labelYYYY-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 examplecdno templates vars tracking. See thetemplatesreference.
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=valueon the command (repeatable), e.g.cdno project create --title T --context work --var ticket=ABC-123;- otherwise, in an interactive TTY,
cdnoasks (“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--varcan’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.
--varonly applies to templated notes.cdno file --attach(the attachment stub) and a plainaction add(no--note) aren’t templated, so--varis 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--varover MCP). A[variables.prompt]placeholder in one of those templates fails at creation (anUnresolvedPromptserror) 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_requiredwith a template default and the occasionalcdno lintand 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
- Configuration — the configurable surface.
- Configuration reference — every
config.tomlkey. - Frontmatter fields — the built-in fields per note type.
normalise,lint.