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

Configuration reference

The vault’s settings live in .cuaderno/config.toml, written by cdno init. Every key is optional — defaults are applied when omitted. For the conceptual tour, see Configuration.

Full example

[vault]
name = "My Research Vault"
method_version = "1.2"             # the RLM version; written by init, changed by `cdno migrate`
max_active_projects = 5            # the active-project cap

# Glob patterns excluded from the index (search, lint, link checks).
# Additive only — no "!" negation. Matched against vault-relative paths.
# NEVER deletes files; only scopes what the index considers.
ignore = ["CLAUDE.md", "README.md"]

# Per-type extra required frontmatter fields. Built-in required fields
# are always enforced; these add vault-specific requirements (cdno lint).
[schemas.project]
extra_required = ["collaborators"]

[schemas.evidence]
extra_required = []

# Typed frontmatter fields for a built-in type. Recognised by the
# Templates editor and type-checked by `cdno lint`.
[schemas.daily.fields.meds]
type = "bool"                     # bool | int | float | string | date
default = false                  # static, type-checked against `type`

[schemas.daily.fields.mood]
type = "string"
values = ["low", "ok", "good"]   # allowed values (a string constraint)
default = "ok"

# How an activity's tracked numbers are read back. Each metric declares how
# it collapses to one point per date; without a declaration the activity's
# series come from its body table, with each column summed.
[tracking.practice]
records  = "detail"               # frontmatter key holding repeated records
group_by = "subject"              # one series per distinct value

[tracking.practice.metrics.minutes]
aggregate = "sum"                 # a TOTAL
unit      = "min"

[tracking.practice.metrics.focus]
aggregate = "mean"                # a RATING; sum would grow with how often you log

# Static template variables — resolve in any custom template ({{author}}).
[variables]
author = "A. Researcher"

# Prompted variables — gathered at note creation (--var, prompt, or error).
[variables.prompt]
collaborators = "Who are the collaborators?"

Keys

KeyTypeDefaultPurpose
vault.namestring"My Vault"A human label for the vault.
vault.method_versionstring "<major>.<minor>"absent (read as 1.0)The version of the Research Logbook Method the vault follows, not cdno’s version. Written by cdno init; changed only by cdno migrate. See Method version.
vault.max_active_projectsinteger5The active-project cap: how many projects may count, that is, be active and name no one else in held_by:.
ignorelist of globs[]Files the index skips. Additive; never deletes. See Ignore globs.
schemas.<type>.extra_requiredlist of strings[]Extra required frontmatter fields for a built-in note type (not enforced on a custom type), enforced by cdno lint.
schemas.<type>.fields.<name>table—A typed frontmatter field for any note type, built-in or custom (type, default, required, values, settable, log_on_change). Recognised by the Templates editor (built-in types), type-checked by cdno lint, and (when settable) writable via cdno frontmatter set. See Typed schema fields.
tracking.<activity>table—Declares how an activity’s tracked numbers are read back (records, group_by, and a metrics.<name> table per metric). Without one, the activity’s series come from its body table with each column summed. See Tracking.
focus.carry_over_daysinteger1How many days back the focus is read from today. 0 reads today only. See Focus.
focus.paused_lookback_daysinteger14How far back a pause is still offered for resume, cdno now and orientation.
focus.max_window_daysinteger366Ceiling for the two windows above; a vault that exceeds it fails to open.
projects.holder_sentinelslist of strings["me", "I", "myself", "(mine)", "someone", "somebody", "nobody", "unknown", "tbd", "n/a", "none", "-", "?"]Values of a project map’s held_by: that name you or nobody, so they read as no holder. Trimmed, any case; [] accepts any name. See Projects.
note_types.<name>table—Declares a config-defined custom note type (folder, required/optional fields, template, …) — a schema-only type for entities the built-ins don’t cover. See Custom note types.
variables.<name>string—Static template variable; resolves in any custom template (per-type values win on name clash).
variables.prompt.<name>string—Prompted template variable; the value is the prompt text. Gathered at creation from --var name=value, an interactive prompt, or a static [variables] default; errors if none supplies it.

Method version

[vault] method_version records the version of the Research Logbook Method the vault follows, as two numbers, "1.1": no patch part and no v. It is the method’s version, not the crate version cdno is released under, and it changes only when the method’s text does.

[vault]
method_version = "1.1"
  • cdno init writes the version this cdno implements. A vault with no key (one created before the key existed) is unstamped and read as RLM 1.0; nothing breaks.
  • Only cdno migrate changes it: run cdno migrate --dry-run to see what it changes, then cdno migrate. Do not edit it by hand, since a migration’s file steps and its log entry go with the stamp. It is never changed when a vault opens, and never over MCP.
  • Every write re-reads the key under the write lock. A vault on an older major version than this cdno is refused until you migrate it; a vault on a newer major is refused until you upgrade cdno. Reads keep working in both cases. A different minor version writes normally; cdno prints a one-line notice on stderr and cdno lint mentions a newer one.
  • A malformed value ("v1", "1.1.0") is an error naming the key.
  • The key stays in [vault], which older binaries read without rejecting unknown keys, so a vault stamped by a newer cdno still opens in an older one.

See Troubleshooting.

Ignore globs

ignore lists files that live in the vault directory but are not notes — repo scaffolding like CLAUDE.md or README.md. They are excluded from the index, and therefore from search, lint and backlinks as well. The files are never touched on disk.

Patterns are matched against each file’s vault-relative path:

PatternMatches
CLAUDE.mdthat file at the vault root
**/*.draft.mda .draft.md at any depth
folder/*/**everything one or more levels below folder/<anything>/
folder/*/*/**everything two or more levels below folder/<anything>/

The last two are the trap worth knowing. * stays inside one path segment but ** is recursive, so portfolios/*/** does not mean “the level below a portfolio” — it matches the portfolio’s own notes as well as anything nested under them. A glob written that way excludes every note in the folder it was meant to tidy, and because an unindexed note is also unsearchable and unlinkable, the symptom looks like a broken view rather than a misconfigured vault.

One thing guards against that: cdno reindex prints how many files the globs excluded, so an over-broad pattern shows up as a number rather than as notes that quietly stopped appearing.

If notes go missing, clear the pattern and run cdno reindex: every row comes back.

Note that attachment artefacts filed into a portfolio need no ignore entry — they are excluded automatically, by location. See vault structure.

Typed schema fields

[schemas.<type>.fields.<name>] declares a typed frontmatter field for any note type, built-in or custom. It is the richer sibling of extra_required: instead of just a name, each field carries a type (and optionally a default and an allowed-value set). Four things consume it today:

  • cdno templates recognises the field (built-in types only; a custom type’s placeholders are its required and optional fields, so declare the field there too), so a custom template referencing {{<name>}} no longer warns “renders literally”;
  • note creation populates the field’s default at create — a custom template referencing {{<name>}} renders that default (a field with no default renders null), so the value lands in the new note’s frontmatter instead of a literal {{<name>}} (built-in types only; a custom type’s default is not written — see Custom note types);
  • cdno lint type-checks the field — a note whose value doesn’t match the declared type (or isn’t one of values) gets a warning;
  • the set_frontmatter setter (cdno frontmatter set, MCP set_frontmatter) writes the field through the index when it is marked settable = true — see the cdno frontmatter reference.
[schemas.daily.fields.meds]
type = "bool"                     # bool | int | float | string | date
default = false                  # optional; static, type-checked against `type`
settable = true                  # optional; allow `set_frontmatter` to write it (default false)
log_on_change = true             # optional; stamp a daily-log line when it changes

[schemas.daily.fields.mood]
type = "string"
values = ["low", "ok", "good"]   # optional; allowed values (only valid on a string)
default = "ok"
required = false                 # optional; default false
Field keyTypeDefaultPurpose
type"bool" | "int" | "float" | "string" | "date"(required)The field’s scalar type. An unknown value is a hard load error.
defaultmatching type—A static default value, type-checked at load. Populated at create when a custom template references {{<name>}}. A date is a quoted "YYYY-MM-DD".
requiredboolfalseReserved for create-time enforcement (a later release); parsed now, but inert — it does not yet block creation.
valueslist of strings—An allowed-value constraint. Valid only on a string field.
settableboolfalseWhether set_frontmatter (cdno frontmatter set, MCP set_frontmatter) may write this field. Default-deny: absent or false means not settable. Never overrides an engine-owned key (type, status, a period key, and a project’s closed) — those stay blocked regardless.
log_on_changeboolfalseWhen a settable field’s value actually changes, stamp a key: old → new line into today’s daily note in the same commit.

Notes and limits:

  • Defaults are static — there is no "today" token; a date default is a literal calendar date.
  • A field only lands in frontmatter if a custom template references it — rendering substitutes the {{<name>}} tokens a template contains; it never adds a frontmatter line. The shipped built-in templates can’t reference vault-specific fields, so populate a declared field by adding a custom .cuaderno/templates/<type>.md that references {{<name>}}.
  • A create-path value wins over a declared default — if the note type’s create path already supplies a value for that name (an engine-supplied placeholder), that value takes precedence and the declared default does not apply. Likewise a [variables] static var of the same name wins over a schema default.
  • A [variables.prompt] name is owned by the prompt — if a field name is also a prompted variable, its value is collected via the prompt (from --var, an interactive prompt, or a static default), and the schema default is not used. This ensures a supplied answer is never discarded.
  • No enum type — model a closed set as a string with values.
  • int and float are distinct — int rejects anything with a fractional part, so a currency amount, a measurement or a rate wants float. A float field accepts a whole number too (82 and 82.5 both validate), because a round reading is written without a decimal point. A non-finite default (nan, inf) is a load error — those values have no meaning as data.
  • List fields are reserved but not yet implemented — a list = true is a load error today.
  • Engine-owned keys are protected — you can’t declare a field named type, or a calendar type’s own period key (daily→date, weekly→week, monthly→month); the vault refuses to open. set_frontmatter additionally refuses to write status for every type — even if a vault declares a status field settable = true — so the lifecycle commands stay its sole writers.
  • extra_required still works and is equivalent to an untyped, non-required string field; on a name clash an explicit fields block wins.
  • A malformed field declaration (unknown type, a mistyped key, values on a non-string, a default that doesn’t type-check) fails at vault-open, like every other config error.

Tracking

[tracking.<activity>] declares how an activity’s numbers are read back. Without one, a tracking note’s series come from the first table in its body, with each column summed — right for a rep sheet, wrong for a balance or a rating. Declaring an activity moves it to frontmatter, where each metric says how it collapses.

[tracking.practice]
records  = "detail"        # frontmatter key holding repeated records
group_by = "subject"       # one series per distinct value of this field

[tracking.practice.metrics.minutes]
type      = "int"
aggregate = "sum"          # a TOTAL
unit      = "min"
plot      = "column"

[tracking.practice.metrics.focus]
aggregate = "mean"         # a RATING - a sum would grow with how often you log
KeyWherePurpose
recordsactivityFrontmatter key holding a sequence of flat records. Omit for plain scalars read straight off the entry.
group_byactivityRecord field the series split on — a category, a subject, a person. One series per distinct value.
atrecordNot a config key but the per-record field that orders a set, so last reads the reading you meant. HH:MM, HH:MM:SS, or a 12-hour time with a meridiem. Needs no quoting — the colon keeps it text — but a colon-less at: 1800 is a number and never reaches the parser. All-or-nothing: cdno lint reports a value it cannot use, or a set only partly stamped.
typemetricbool | int | float | string | date. Optional.
aggregatemetricsum | mean | last | max | min. Defaults to sum.
group_bymetricOverrides the activity’s. "none" collapses across records for an entry-level series.
derivedmetricAn expression computing this metric from sibling fields, e.g. "km * rate_per_km". Evaluated per record, before aggregation. Declare type on it and the vault refuses to open.
unitmetricDisplay unit (min, kg, EUR). Carried through to the chart and the MCP series.
labelmetricDisplay name for the series, when the metric’s key is not what you want on a chart (resting_hr → Resting heart rate).
plotmetricnone | line | column | area | scatter. Defaults to none. Chooses the mark the chart draws, and whether a chart consumer draws it at all — see the note below.

Derived metrics

Some tracked quantities are products of others — a cost from a rate and a distance, a load from a weight and a count. Rather than making whoever writes the entry pre-compute them, declare the expression:

[tracking.commute.metrics.km]
aggregate = "sum"

[tracking.commute.metrics.rate_per_km]
aggregate = "last"

[tracking.commute.metrics.cost]
derived   = "km * rate_per_km"
aggregate = "sum"
unit      = "EUR"

It is evaluated per record, then aggregated — not derived from the aggregates. With two trips of 10 km at 0.50 and 20 km at 0.25, cost is 10.00; deriving from the totals would give a different, wrong number the moment the rate varies.

The grammar is deliberately tiny — one binary operation, nothing else:

expr    := operand OP operand
operand := field-name | number
OP      := '+' | '-' | '*'
  • No /. Division is the one operator that manufactures NaN and infinity, and those must never reach an aggregate. Multiply by the reciprocal, or pre-compute the ratio.
  • No parentheses, calls, chaining or recursion, and no deriving from another derived metric.
  • Field names are letters, digits and _, not starting with a digit. Hyphens are excluded because a-b would be indistinguishable from a subtraction.
  • Numbers are plain decimal — digits, an optional leading -, an optional .. Exponent notation (1e-3) is not supported; write the value out in full.
  • Every operand must name a metric the same activity declares. A typo is a vault-open error naming the field, rather than a silently empty chart. The cost of that requirement: an operand that exists only to be multiplied — a rate, say — still becomes a metric of its own. Leave its plot undeclared (the default) and it is not charted alongside the result — see below.
  • type must be omitted — the output is numeric by construction, so declaring one can only contradict it.
  • A record missing an operand contributes nothing — a gap, on the same rule as a plain metric. So does a result that is not finite.

Choosing the aggregate is the whole point, and it follows from what the number is:

KindExamplesAggregate
Totalamount spent, pages read, minutes practisedsum
Levelaccount balance, a measurement, a top setlast, max
Rate or ratinga score out of ten, perceived difficultymean

Notes and limits:

  • An absent [tracking] section is not an error — an undeclared activity keeps being served from its body table, so nothing forces a migration.
  • Declaring is checked at vault-open, not at first chart render: an unknown aggregate, a mistyped key, or a blank records/group_by fails when the vault is opened, naming the key.
  • An activity may declare no metrics at all. That is a complete use — recording that something happened, with nothing to aggregate.
  • window is reserved for a future time-reduction axis (month-over-month deltas, rollups) and is a load error today, so adding the behaviour later is not a breaking change.
  • A declared activity’s body table is no longer read once its frontmatter yields a series, so the same metric can never appear twice under two disagreeing numbers. This rule is unconditional — it keys on the frontmatter derivation’s full produced set, not on what any individual metric’s plot says, so a declared-but-unplotted metric still suppresses its body-table equivalent.
  • plot chooses the mark, and gates whether it is drawn at all. A declared line/column is used as the chart’s mark (an area or scatter resolves to the closest of the two the chart draws). plot = "none" — the default for a declared metric that names no mark — is still emitted and still queryable over MCP, but left out of any chart pane. Declaring an activity is an explicit act, and is allowed to change what is drawn: its frontmatter series replace its body-table ones (the rule above, unaffected by this), and only the metrics that opt into a mark are charted.

Focus

The [focus] section sets how far back the focus is read. All three keys are optional, and an unknown key in the section is a hard error, so a typo cannot silently turn a window off.

[focus]
carry_over_days = 1
paused_lookback_days = 14
max_window_days = 366
KeyDefaultMeaning
carry_over_days1The focus is the newest open start or resume among the daily notes from today - carry_over_days to today, unless a close follows it. 1 means a start left open yesterday is still the focus this morning and expires after that; 0 reads today only, as before the window existed. Calendar days, not rolling hours.
paused_lookback_days14How far back a pause is still offered: by cdno action resume, by the “Last paused” line of cdno now, and as last_paused in the MCP orientation and project-context reads. 14 covers a weekend and most holidays; raise it if you are away longer.
max_window_days366A ceiling on the two windows. A vault whose carry_over_days or paused_lookback_days exceeds it fails to open, with an error naming the key, its value and the ceiling, so a typo like 14000 cannot turn every read into thousands of file checks. Raise it if you really want a longer window.

Both windows are read from the daily log each time; nothing is cached, so changing a value takes effect on the next cdno command. A running MCP server keeps the config it started with, so restart it (reconnect the client) for an agent to see the change. See cdno now and Troubleshooting.

Projects

The [projects] section holds policy for project maps. Its one key, holder_sentinels, lists the values of a map’s held_by: (the name of whoever else is moving the map) that name you or nobody. Such a value reads as no holder, so the map stays yours and counts, exactly as a map with no held_by: line does. The key is optional; leaving it out gives the default below, so a vault that never sets it behaves as before. An unknown key in the section is a hard error, as in [focus], so a typo cannot silently keep the default.

[projects]
holder_sentinels = [
  "me", "I", "myself", "(mine)",
  "someone", "somebody", "nobody", "unknown", "tbd", "n/a", "none", "-", "?",
]
KeyDefaultMeaning
holder_sentinelsthe list aboveValues that name you (me, (mine), which is how the log writes “no holder”) or nobody (tbd, ?). Each value is trimmed and compared in any case, so " TBD " matches tbd. Setting the key replaces the whole list: add your own name or your own language’s words (holder_sentinels = ["yo", "nadie"]), or set [] to accept any name.

The limit. cdno cannot know your own name. held_by: "<your name>" reads as someone else, so the map stops counting, unless your name is on the list. If you want it refused by cdno project hand-off and read as yours, add it (and keep the default entries you still want, since setting the key replaces the list).

A blank held_by: names nobody under any list, so it is not in the default and you need not add it. A held_by: that is not a plain name (an unquoted [[people/ana]], a number, a date, quoted or not) also reads as no holder; quote a wikilink (held_by: "[[people/ana]]") for it to count as a name.

Templates

Templates live in .cuaderno/templates/ and are pure variable substitution. cdno init writes two starters (daily.md, and concept.md for the concept type it declares); other built-in types use their built-in default until you add a file. cdno selects the most specific template that exists: a custom variant (e.g. tracking-gym.md), then a custom type (e.g. project.md), then the built-in variant default, then the built-in type default. Template field order is the canonical order cdno normalise enforces.

The per-type placeholders that resolve at creation, with a worked example, are in Customising templates and frontmatter. An unknown placeholder is left verbatim in the note.

Static [variables] resolve in any custom template (e.g. {{author}}). Prompted [variables.prompt] are gathered at creation (via --var name=value, an interactive prompt, or a static default) — see the tutorial.

Editing from the CLI

cdno config edits this file without hand-editing it, and every write goes through the same gate:

cdno config show                    # verbatim, comments and key order intact
cdno config validate                # the exact check running a vault performs
cdno config edit                    # $EDITOR round trip
cdno config note-type set --name people --folder people --required name,email
cdno config field set --note-type people --field email --type string
cdno config var set --name author --value "Your Name"

Saving is gated so an edit can never leave the vault unopenable:

  1. The whole candidate is validated first — TOML parse, ignore globs, and the [note_types.*] / [schemas.*] rules. If it would not reopen, the save is refused and the file is left untouched.
  2. A content-hash compare-and-swap then guards against a concurrent hand-edit: if the file changed on disk since it was read, the save is refused rather than overwriting the newer version.
  3. Only then is the candidate written verbatim, so comments and key order survive.

The structured setters merge: a flag you do not pass keeps its current value, so changing one key cannot silently drop the rest of an entry. Pass an empty value to clear one (--template ''), or the paired negative flag for a boolean (--no-append-only).

See the config reference for every subcommand.

See also