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 two starter templates —.cuaderno/templates/daily.md, andconcept.mdfor theconceptcustom 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 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}}
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 normaliseonly reorders frontmatter keys; it won’t add a new section like## Risksto old notes.)
closed:in a customised project template. The built-in project template ends its frontmatter withclosed: null, the date a project is completed or dropped. A.cuaderno/templates/project.mdyou 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 lintandcdno templates listsay when your template lacks it, andcdno templates sync projectadds it next to its built-in neighbour (aftercore_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: 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}}
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_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 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
- Configuration — the configurable surface.
- Configuration reference — every
config.tomlkey. - Frontmatter fields — the built-in fields per note type.
normalise,lint.