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

Cuaderno

Cuaderno (cdno) is a command-line vault manager for the Research Logbook Method (RLM) — a way of running long-horizon knowledge work out of a folder of plain Markdown files. It is built for researchers and other deep-work practitioners, and is deliberately friendly to ADHD working styles: it leads with what is there, not what is missing — no guilt counters, no angry overdue badges, explicit permission to park or drop work.

You drive it from a terminal, and an AI assistant (Claude and other MCP clients) can drive it too, through the bundled MCP server.

What it gives you

  • A dated, append-only log as the single source of truth for what you did.
  • Projects with a current state, next actions, milestones, and waiting-on items — capped at five active at a time so you can’t overcommit.
  • Evidence portfolios: per-question dossiers that accumulate papers, experiment results, and notes over months and years.
  • Important questions kept front-and-centre and re-read often.
  • Stewardships: small, bounded, long-haul responsibilities (health, finances) with optional habit tracking.
  • A commitments register — promises with dates, aggregated from everywhere they live.
  • Full-text search, frontmatter linting, and a JSON mode so every read and write verb is scriptable.

Two ways to use it

SurfaceWhat it isStart here
cdno CLIThe terminal tool for the daily loopQuickstart
cdno-mcp serverAn MCP server so Claude can read and write your vaultConnect to Claude

Both operate on the same Markdown vault — the files on disk are always the source of truth (the SQLite index is just a rebuildable cache).

How this guide is organised

  • Getting started — install, create a vault, run the daily loop, and wire up Claude.
  • Concepts — the mental model: the method, the twelve note types, the vault layout, and the rules the tool enforces.
  • Tutorials — task-oriented walkthroughs of each workflow.
  • CLI reference — every command, flag, and example.
  • MCP server reference — every tool exposed to AI clients.
  • Appendix — JSON shapes, frontmatter fields, the full config file, recurrence syntax, and troubleshooting.

This guide documents the shipped behaviour of Cuaderno. New to it? Read The Research Logbook Method for the why, then jump to the Quickstart for the how.

Installation

Cuaderno ships two binaries:

  • cdno — the command-line tool for the daily loop.
  • cdno-mcp — the MCP server that lets Claude (and other MCP clients) read and write your vault.

Both are installed together.

Homebrew (macOS and Linux)

The recommended path. Pre-built bottles exist for macOS (Apple Silicon + Intel) and Linux (x86_64 + aarch64).

brew install agustinvalencia/tap/cuaderno

Verify:

cdno --version      # -> cdno 0.1.24 (or newer)
cdno-mcp --version

To upgrade later:

brew upgrade cuaderno

From source

Use this on platforms without a bottle, or to track the latest main. You need a Rust toolchain.

git clone https://github.com/agustinvalencia/cuaderno
cd cuaderno
cargo build --release --bins
# Binaries land at target/release/cdno and target/release/cdno-mcp

Put them on your PATH — for example:

ln -s "$PWD/target/release/cdno"     /usr/local/bin/cdno
ln -s "$PWD/target/release/cdno-mcp" /usr/local/bin/cdno-mcp

Shell completions (optional)

cdno can print a completion script for your shell, with vault-aware suggestions (it completes project, portfolio, and stewardship slugs by re-invoking the binary on TAB):

# zsh — add to your ~/.zshrc:
source <(cdno completions zsh)

Supported shells: bash, zsh, fish, elvish, powershell. See completions for per-shell setup.

Uninstall

brew uninstall cuaderno          # if installed via Homebrew
# or, for a source build, remove the symlinks you created:
rm -f /usr/local/bin/cdno /usr/local/bin/cdno-mcp

Uninstalling the binaries never touches your vault — it’s just Markdown files on disk. Delete the vault directory yourself if you want it gone.

Next step

Create your vault: Initialise a vault.

Initialise a vault

A vault is just a directory of Markdown files with a .cuaderno/ config folder at its root. Create one with cdno init:

cdno init ~/notebook
cd ~/notebook

This scaffolds the full folder tree, writes a default .cuaderno/config.toml (which declares the concept type), and writes two starter templates into .cuaderno/templates/: daily.md and concept.md. Every other note type renders from its built-in template until you add a file for it there. What you get:

~/notebook/
├── journal/          # daily + weekly notes, partitioned by year
│   └── 2026/         #   e.g. journal/2026/daily/2026-04-25.md, journal/2026/weekly/2026-W17.md
├── projects/         # project maps (max 5 active)
│   ├── _parked/      # inactive projects
│   └── _done/        # closed projects, partitioned by year
├── actions/          # manifest action notes (the heavy form)
│   └── _done/        # completed actions, partitioned by year
├── portfolios/       # evidence dossiers, one folder per question
├── stewardships/     # long-haul responsibilities (flat file or folder)
├── commitments/      # standalone promises with deadlines
│   └── _done/        # fulfilled commitments
├── questions/
│   ├── research/
│   └── life/
├── inbox/            # quick captures awaiting triage
├── concepts/         # the concept library (a custom type init declares)
└── .cuaderno/
    ├── config.toml   # vault configuration
    └── templates/    # note templates (override the built-ins here)

See Vault structure for what each folder holds.

Running cdno from anywhere

You rarely need to be at the vault root. cdno finds the vault by walking up from your current directory until it sees a .cuaderno/ folder — so commands work from any subdirectory.

When you’re outside any vault, two fallbacks apply, in order:

  1. The --vault <PATH> flag (highest priority — overrides everything).
  2. The CUADERNO_VAULT_PATH environment variable.
# From anywhere, target a specific vault:
cdno --vault ~/notebook log "spotted a bug in the sampler"

# Or set it once for the shell session:
export CUADERNO_VAULT_PATH=~/notebook
cdno log "spotted a bug in the sampler"

If you’re standing inside vault A while CUADERNO_VAULT_PATH points at vault B, the directory you are in wins — writes land in A. The env var is only a fallback for when discovery finds nothing.

Upgrading an older vault

cdno init refuses to run on a vault that already exists: re-initialising is destructive, so it is never done for you. When a newer cdno ships a note type that init now declares, add it to your existing vault instead:

cdno config note-type install --list             # what ships, and whether this vault has it
cdno config note-type install --name concept

It writes the type’s template, its folder and its declaration, each only when absent. A declaration you already have, or a template you have edited, is kept and reported, never overwritten, so the command is safe to re-run; it ends with concept: already installed, nothing to do when there is nothing left to write. Add --dry-run to see exactly what it would write first. See Installing a bundled note type.

Back up your vault

A vault is just Markdown files (the SQLite index in .cuaderno/index.db is a rebuildable cache — see Business rules). The simplest, most durable backup is version control: git init the vault and commit as you go, or keep it in a synced folder.

cd ~/notebook
git init && git add . && git commit -m "Initial vault"
# The index is regenerated on demand, so it's safe to ignore:
echo ".cuaderno/index.db" >> .gitignore

Because the Markdown is the source of truth, your history is just your commits — nothing is locked inside a proprietary store.

Next step

Run your first daily loop: Quickstart.

Quickstart: the daily loop

This is Cuaderno in five commands. It assumes you’ve installed cdno and created a vault. Run these from inside the vault (or with --vault).

# 1. Start a project (capped at 5 active).
cdno project create --title "Surrogate model" --context work

# 2. Give it a next action, tagged by the energy it needs.
cdno action add --project surrogate-model \
                --title "Run feature set B on the full mesh" \
                --energy deep

# 3. Record a promise with a deadline.
cdno commit create --title "Pay rent" --due 2026-06-01 --context personal

# 4. Each morning: see what's due, your active projects, and a suggested start.
cdno orient --energy deep

# 5. Mark the action done when it's finished (substring match on the bullet).
cdno action complete --project surrogate-model --query "feature set B"

Throughout the day, the two verbs you’ll reach for most:

cdno log "scaled the mesh to 2M cells; runtime 4x, still stable"   # append to today's log
cdno capture "check whether Chen 2025 used the same preconditioner" # drop a thought into the inbox

Interactive vs. scripted

Every write command follows the same convention (see CLI overview):

  • In a terminal, omit a required flag and cdno prompts you for it, then asks you to confirm before writing.
  • In a script, pipe, or with --no-interactive, a missing required flag is an error instead — so automation never blocks on a prompt.
# Interactive: cdno asks for the title and context.
cdno project create

# Scripted: everything supplied, no prompts.
cdno project create --title "Surrogate model" --context work --no-interactive

Machine-readable output

Add --json to any read or write verb to get structured output instead of a formatted table. Read verbs emit their listing/detail; write verbs emit a { "path": ..., "message": ... } result and run non-interactively. Great for scripts and for piping into jq.

cdno project list --json | jq '.[].slug'
cdno project create --title "Surrogate model" --context work --json
# -> { "path": "projects/surrogate-model.md", "message": "Created projects/surrogate-model.md" }

See JSON output for the full shapes.

Where to go next

Connect to Claude (MCP)

Cuaderno ships an MCP server, cdno-mcp, that exposes your vault to AI clients — Claude Desktop, Claude Code, Kiro, Gemini CLI, and anything else that speaks MCP. The assistant can then read context (your orientation, a project, a portfolio) and make writes (append to the log, file evidence, complete an action) on your behalf.

It’s the same engine as the CLI — the files on disk stay the source of truth.

Register the server

Add cdno-mcp to your client’s MCP configuration. For Claude Desktop, edit ~/.claude/claude_desktop_config.json (Claude Code: ~/.claude.json or per-project .mcp.json):

{
  "mcpServers": {
    "cuaderno": {
      "command": "cdno-mcp",
      "env": { "CUADERNO_VAULT_PATH": "/Users/you/notebook" }
    }
  }
}
  • command must resolve on the client’s PATH. If it doesn’t, use the absolute path (e.g. /opt/homebrew/bin/cdno-mcp, or target/release/cdno-mcp for a source build).
  • CUADERNO_VAULT_PATH tells the server which vault to open. You can omit it; the server then opens whichever vault its working directory belongs to (the same discovery rule as the CLI).

Restart the client. Cuaderno’s tools then appear in the client’s tool list.

What the assistant can do

The server advertises 66 tools, grouped by purpose:

  • Context-gathering reads — get_orientation, get_project_context, get_portfolio_contents, get_weekly_context, search_notes, read_daily_note, and more.
  • Writes — append_to_log, file_to_portfolio, update_project_state, add_action, complete_action, create_commitment, create_tracking_entry, the daily/weekly section writers, and others.
  • Creation and lifecycle — create_project, create_portfolio, create_question, create_stewardship, park_project, activate_project, set_question_status, and so on.

The full catalogue, with each tool’s inputs and output shape, is in the MCP server reference.

Skills

You can wrap common multi-step flows as Claude skills that call these tools — e.g. a daily orientation that reads get_orientation and writes your intention with upsert_daily_section. The repo ships worked examples under examples/skills/; see Using with Claude skills.

Next step

Learn the model behind it all: The Research Logbook Method.

The Research Logbook Method

The Research Logbook Method (RLM) is the practice Cuaderno implements. It distils habits common to prolific researchers — Faraday’s notebooks, Darwin’s dated entries, Hamming’s “important problems,” Knuth’s and Tao’s working logs — into eight concrete practices across two tracks (seven in RLM 1.0; see Method version). Each maps onto one or more note types the tool manages.

Two tracks

Research is two jobs at once. Inquiry is open-ended, driven by questions and evidence. Operations is delivery — deadlines, collaborators, promises. Most systems handle one and force the other into its shape. The RLM keeps them separate and lets them share a substrate: the daily log, the weekly review, and one set of projects that bridges the two.

Track 1 — Inquiry: how you investigate

  1. A chronological log (Faraday). A single append-only stream of what you did, observed and thought, in the order it happened. Past entries are never edited; a change of mind is a new entry referencing the old one. The one exception is an address: when a page is renamed, every reference to it is corrected to the new name, the old name is kept on the page, and the rename is itself an entry. It removes the “where do I put this?” decision, preserves the reasoning and not just the result, and keeps you honest against hindsight. Unified across contexts — the regularisation weight and the plumber go in the same day.
  2. Evidence portfolios (Darwin). One folder per active question, accumulating whatever is relevant. The log interleaves many questions; the portfolio aggregates one. Name the folder practically, but phrase the question at the top of its index: “sparse models” names a topic, “does the sparse variant outperform the dense baseline out of distribution?” tells you when you are done. Don’t organise inside — accumulate, and file as things arise, while your reaction is still the most valuable part.
  3. Important questions (Hamming). The few questions that would genuinely change your situation if answered. Not tasks — questions. They sit above projects: one question may spawn several over time and persists as they finish or get shelved. Two short lists, research and life, reviewed monthly: are these still the right questions?

Track 2 — Operations: how you deliver

A question and a project are not the same thing. A question is open-ended and may branch or turn out to be the wrong question. A project is bounded — a deliverable, a deadline, people counting on you. One question can span several projects; a project can draw on several portfolios; either can exist without the other. Keeping them apart stops deadline pressure polluting open-ended inquiry, and stops open-ended inquiry dissolving operational commitments.

  1. Project maps (Knuth). A mutable one-pager per piece of finite work — the page you open after a two-week gap. It answers: where was I, what do I believe and what would change my mind, what are my next actions, what is time-sensitive, where is everything. Tasks live here, embedded in the project that gives them meaning. Maximum five active projects, at most three next actions each — fifteen tasks as a ceiling, not a backlog. A project map is not a history (the log is), not a long checkbox list (eighteen unchecked boxes reads as failure), and not a timeline.

    Parking, clarified (1.1). A parked project map is a complete map. Parking changes two things only: the project does not count against the five, and it is read at the monthly review rather than daily. Everything else a map does, it keeps. You may write next actions on a parked map, update its state, note a milestone, note who you are waiting on. A hard-dated milestone on a parked map, or a dated action on it, is still a promise and stays in the commitments register.

    A map you start work on is a map that counts: being in the middle of something is the strongest sign you are moving it yourself. If you find yourself working on a parked map, or picking up work you had set down on it, it is not parked; activate it, parking another if you must.

    On adopting this clarification, read the parked maps once: every stale date on them is dropped, with its entry in the log, or re-dated.

  2. Stewardships. Dashboards for perpetual responsibilities — health, finances, household — that never finish. The critical distinction: projects end, stewardships do not. If health and finances occupy project slots you permanently lose two of five to things that cannot complete. They run in the background with periodic commitments and get a scan at the weekly review, without competing for slots.

  3. A commitments register. One flat, date-sorted list of everything with a hard external deadline, whichever project or stewardship it came from. This is not a to-do list, and the distinction matters: a to-do is something you decided to do, a commitment is something someone else is counting on. Mixed together, the commitments drown and you break promises you meant to keep.

  4. Energy-matched scheduling (Tao, Wolfram). Tag work deep, medium or light and match it to your current cognitive state. Forcing deep work when your brain wants something mechanical is not discipline, it is waste — the alternative to deep work on a low day is usually paralysis, not virtue. The exception: if you never have deep days, that is environmental, and the fix is protecting blocks rather than trying harder.

  5. Handing off (1.1). A project map answers: where was I, what do I believe and what would change my mind, what are my next actions, what is time-sensitive, where is everything. It gains one more question: whose move is it?

    A project you have handed to someone else carries their name at the top of its map: held by: the harbour office, held by: a colleague, held by: the contractor who has the hull. The holder is whoever you could write in Waiting On. A map with no name on it is yours; a map that names you is yours. The page stays where it is; only the name changes.

    Why: if a project someone else is moving holds one of your five, the cap counts their attention as yours, and you will either raise the number or stop writing such work down. Both are worse than a name on a page.

    The count. Count the maps that are neither parked nor handed off. A handed-off project is still open: it keeps its map, keeps logging, keeps its dates in the register, and is still read at the weekly review, and at the daily orientation when something has come back. It is not a sixth slot; it is a project that is not a unit of your attention this week.

    The review. Yours: did it move? Handed off: what came back, and what is waiting on your decision? The second is asked in a week when the map changed.

    The log. Handing off and taking back are dated entries in the chronological log naming the project and the holder; taking back strikes the name. Taking back a parked map only strikes the name; to work on it, activate it as well, as its own entry.

    The cap is checked whenever a map that did not count begins to count: activating a map nobody else holds, or taking back an active one.

    Three guards. If you cannot name who has it, it is yours. A handed-off map says what you are waiting for; a name over an empty Waiting On is a parked map wearing a name, and the weekly review parks it or takes it back. And handing off is never the answer to “the cap is full”: doing a thing on a handed-off map does not make it yours, doing most of its work does; at the weekly review, a handed-off map you worked on yourself on two or more days is taken back, or its name changed to who really has it. You do not hand off a map while you are in the middle of its work.

    The asymmetry. Parking is your decision about now, so starting work on a parked map undoes it at once; a holder is a fact about someone else, which one afternoon’s work on the map does not change.

    The map already has a Waiting On section for single items. Handing off is Waiting On lifted to the whole project, which is why the method already half-had it. Its ancestry is GTD’s Waiting For, kept per project rather than as a separate list.

The bridge

The log feeds both tracks: an observation becomes evidence in a portfolio, a realisation that something must be rerun before a deadline becomes a next action on a project map. The weekly review governs both — retrospective and forward scan. Projects reference the portfolios they draw on, and commitments decompose into milestones that surface as next actions at the right time.

The daily orientation is where the tracks meet: check what is urgent, glance at what is important, check your energy, and pick one thing.

Actions, not tasks

The RLM speaks of actions, not “tasks,” on purpose. The default form of an action is a single inline bullet on a project map — not a heavyweight per-item note. You only “promote” an action to its own note when it grows into an investigation spanning multiple days and evidence artefacts. This keeps the friction of capturing the next step near zero. See Actions.

Designed for ADHD working styles

The method is deliberately shaped to be sustainable when executive function is unreliable:

  • Leads with what is there, not what is missing — no guilt engine, no red overdue counts.

  • Permission to park or drop. Projects park, complete or drop, questions retire, actions and milestones drop, commitments get fulfilled or dropped — all first-class, and none of it is a failure state.

    “Reversible” is worth stating precisely, because it means less than it might sound like. Parking is genuinely two-way: a parked project activates again, and a retired question does too. Dropping is not — there is no un-drop verb for an action, a milestone or a commitment, and none is planned. It is re-decided rather than undone: if a dropped thing turns out to matter, make it again, and the record honestly shows two decisions rather than pretending the first never happened. That is the same reason a drop is logged as a drop and not as a completion.

    Projects are the one named exception. A dropped project keeps its slug, its map and its history, so cdno project activate brings it back, and a project dropped in error can later be completed, or the reverse. Each is still a new decision with its own log line, never an erasure of the first.

    What a drop does preserve is the note. A dropped action or commitment keeps its file, its body and its created date, archived under _done/<year>/ — so the context you wrote survives even though the plan did not. A milestone is only a bullet, with no note behind it, so dropping one leaves just the log entry naming it — including when it had sub-bullets, which is the one place this guarantee does not yet hold (#574).

  • Minimal maintenance. If keeping the system running costs more than a few minutes a day outside the weekly review, something is wrong.

  • One obvious next step. cdno orient answers “what should I do now?” with a single suggestion, biased to your current energy.

The rhythm

The method runs on a few interlocking loops:

  • Daily — orient in the morning, log and act through the day, a light close at the end. See The daily loop.
  • Weekly — a short retrospective (wins, challenges, one improvement) and a single goal for next week. See Weekly review.
  • Monthly — the parked maps (“is it time?”) and the questions (“still the right questions?”).
  • Occasional — file evidence as you find it, triage the inbox, prune questions and projects.

Method version

The version tracks the method’s text, not any tool. A change is minor if every page of an existing logbook can be read under it without being rewritten, moved, or given a line it lacks; a minor version may ask for a one-time read-through of existing pages. A change is major if any page must be rewritten or moved, and it comes with instructions for doing so (in a tool, a migration). A logbook records on its first page, dated, the version it follows and the day it moved to a newer one; the published method keeps a changelog of practices.

Changelog of practices

  • 1.0 — the seven practices: the chronological log, evidence portfolios, important questions, project maps, stewardships, the commitments register, and energy-matched scheduling.
  • 1.1 — parking clarified (under practice 4), practice 8 (handing off), and the monthly loop in the rhythm. Additive: every 1.0 logbook is a valid 1.1 logbook. On adopting 1.1 a paper user does two things: a dated line on the first page, and one pass over the parked pages’ dates.
  • 1.2 — practice 1 gains its one exception: a renamed page’s references are corrected, the old name is kept on the page, and the rename is itself an entry. Additive: every 1.1 logbook is a valid 1.2 logbook. On adopting 1.2 a paper user does one thing: a dated line on the first page; there is nothing to read through.

How it maps to the tool

PracticeNote type(s)Primary commands
Chronological logdaily, weeklylog, orient, review
Evidence portfoliosportfolio, evidenceportfolio, file
Important questionsquestionquestion, questions
Project mapsproject, actionproject, action
Stewardshipsstewardship, trackingstewardship, track
Commitments registercommitment (+ computed)commit, commitments
Energy-matched scheduling(an energy on actions)orient --energy, action add --energy
Handing offproject (a held_by holder)project hand-off, project take-back

Read on: Note types.

Note types

Every note in a vault has a type: in its frontmatter. Cuaderno parses that frontmatter into a typed structure — if it parses, it’s valid. There are twelve note types.

TypeLives inMutabilityPurpose
dailyjournal/<year>/daily/Append-onlyOne day’s chronological log
weeklyjournal/<year>/weekly/Append-onlyWeekly review (Wins, Challenges, One Improvement, This Week’s Goal)
monthlyjournal/<year>/monthly/Append-onlyMonthly review (Wins, Themes, Next Month’s Focus) + links to the month’s weeks
projectprojects/ (+ _parked/, _done/<year>/)MutableProject map: state, next actions, milestones, waiting-on
actionactions/ → actions/_done/<year>/Mutable while open, then archivedManifest note for an action-as-investigation
portfolioportfolios/<slug>/_index.mdOccasionally editedIndex/summary of an evidence dossier
evidenceportfolios/<slug>/Append-onlyA single piece of evidence (paper, result, note)
stewardshipstewardships/ (flat or folder)Occasionally editedDashboard for a perpetual responsibility
trackingstewardships/<slug>/tracking/Append-onlyOne time-series entry (a gym session, a measurement)
questionquestions/research/ or questions/life/Status transitionsAn important research or life question
commitmentcommitments/ → commitments/_done/Moves when it ends, kept or droppedA standalone dated promise

Plus the inbox: raw, untyped captures in inbox/ awaiting triage.

The journal: daily, weekly, and monthly

The chronological backbone. Daily notes collect timestamped log lines plus structured sections (Intention, Agenda, Standup, Meeting), and a ## Notes section for worked-out substance that the log points at (see The concept library). Weekly notes hold the review (Wins, Challenges, One Improvement) and the week’s single goal. Monthly notes hold the higher-altitude review (Wins, Themes, Next Month’s Focus) and a ## Weeks block that links the month’s weekly notes rather than copying them, so the weeks stay the source of truth. All three are append-only — the historical record only grows.

project — the one mutable map

Projects are the only freely-mutable note type. A project carries a Current State, a list of next actions (inline bullets by default), milestones, and waiting-on items. When you update the Current State, the previous state is auto-logged to today’s daily note first, so history is never lost (see Business rules). At most five projects are active at once; the rest live parked in projects/_parked/, and a completed or dropped project moves to projects/_done/<year>/.

action — inline by default, a note when it grows

An action’s default form is a checkbox bullet on its project map. That’s usually all you need. When a single action becomes an investigation spanning days and artefacts, you promote it to a manifest action note (heavier: status, energy, criteria, links). Completing an action removes the bullet, logs it, and — if it had a note — archives that note to actions/_done/<year>/. See Actions.

portfolio + evidence — dossiers per question

A portfolio is a folder named for a question, with an _index.md (the portfolio note) and a set of evidence notes filed into it over time. Each evidence note records a source and an origin (a wikilink to whatever produced it). Evidence is append-only — a portfolio is a growing record. See Research and evidence.

stewardship + tracking — long-haul responsibilities

A stewardship is a dashboard for something you tend indefinitely. It can be flat (a single stewardships/<slug>.md) or expanded (a stewardships/<slug>/ folder with _index.md, a tracking/ subfolder for time-series entries, and a routines/ subfolder for reference docs). Expanded stewardships accept tracking notes via cdno track. See Stewardships and tracking.

question — important questions, kept visible

A question note has a domain (research or life) and a status (active, parked, answered, retired). Questions anchor portfolios and projects (via the core_question link). List the active ones any time with cdno questions.

commitment — dated promises

A standalone promise with a hard due: date and a context. It moves to commitments/_done/<year>/ when it ends — stamped completed if it was kept, dropped if it was cancelled or overtaken. Both are terminal; the frontmatter says which. Standalone commitments are one of four sources feeding the aggregated commitments view.

These twelve are a closed set with built-in behaviour. For an entity they don’t cover (people, books, clients), you can declare a schema-only custom note type in config.toml — a folder, field rules, and a template, with no recompile (see the worked Tracking people recipe).

One custom type comes declared for you: cdno init writes a concept type (concepts/, mutable, refined in place) into a new vault’s config, for understanding you will reuse — a theorem, a technique, a procedure. It is an ordinary custom type you may delete. See The concept library.

For the exact frontmatter fields of each type, see Frontmatter fields. Next: The concept library.

Which notes can be renamed

cdno note rename takes:

TypeRenamable?
projectYes.
action notesActive ones only; an archived action note (actions/_done/) is refused.
questionA portfolio with the same slug moves with it, as a pair.
portfolioDirectly, and its folder moves with it. A portfolio that shares a question’s slug is renamed through the question instead.
custom typesAny type declared in [note_types.<type>], as a flat note; a custom note kept as a folder (<dir>/<slug>/_index.md) is refused.

Not renamable: daily, weekly and monthly (the journal is addressed by date), archived action notes, commitment, stewardship, evidence, tracking, inbox items, folder notes (other than a portfolio’s) and a note with no type. They are refused with note_not_renamable, and nothing is written. A rename corrects references, which is the one exception to the append-only rule; see the method page.

The concept library

Some of what you work out is neither evidence nor work. What the Woodbury identity buys you when you invert a low-rank update. How to do a rolling restart of a StatefulSet without losing quorum. It answers no open question, so it is not evidence. It has no deliverable and never completes, so it is not a project or an action. It belongs to no perpetual responsibility, so it is not a stewardship routine. Left in the daily log, it is findable but scattered: three partial versions across three days, and the one you find first is not necessarily the best.

The cost of that is re-derivation — working the same thing out again because the last time is buried, or because the half-versions disagree. A concept note is the fix: the single, current, best account of one piece of understanding you expect to reuse. Concept notes together form a library of concepts in a flat concepts/ folder, organised by tags and links rather than by folders.

A concept note is refined in place as your understanding improves, and each refinement leaves a one-line trace in the daily log. It has no lifecycle, no staleness, no review obligation and no cap.

A custom type, declared for you

concept is not a thirteenth built-in note type. It is an ordinary custom note type that cdno init declares in a new vault’s .cuaderno/config.toml, with its template installed as .cuaderno/templates/concept.md:

# A declared custom type: the concept library (RFC 0002). Delete this block (and
# any notes under concepts/) if you do not want one; nothing else depends on it.
[note_types.concept]
folder = "concepts"
required = ["created"]
optional = ["tags", "origin"]
template = "concept.md"

If you do not want a library, delete the block. A vault created before the type existed gets the declaration, the template and the folder with one command, which writes only what is absent and never changes a declaration you already have (see Installing a bundled note type):

cdno config note-type install --name concept

A concept note looks like this:

---
type: concept
created: 2026-09-25
tags: [linear-algebra, optimisation]
origin: "[[journal/2026/daily/2026-09-02#Woodbury identity]] [[journal/2026/daily/2026-09-24#Low-rank refit]]"
---

# Woodbury identity

## Statement
…

## Why it matters
…

## See also
- [[concepts/sherman-morrison]]

The title is the H1. tags is your subject vocabulary: open, indexed, and needing no configuration. origin, written only when you supply it at creation, holds wikilinks to the daily-note entries the concept came from. The body is free-form: Statement / Why it matters / See also is the template’s default, and a procedure or a definition simply uses its own headings.

One concept per note. If a note needs two headings that could each be cited on their own, it is usually two notes, linked. A single derivation that does not split can stay one note, with its sections cited by heading link ([[concepts/woodbury-identity#Statement]]).

The filing test

Before you keep something, ask what is it?

  1. A dated observation, result or source that bears on an open question → evidence in a portfolio.
  2. The answer to a question you weighed evidence for → the question note, with status: answered. Its ## Current Thinking may be promoted into a concept that the question links to; the question keeps the record of how it was answered.
  3. A prescribed practice belonging to one stewardship (a workout plan, a care routine) → a stewardship routine.
  4. Understanding you will reuse, independent of any deliverable — a theorem, a definition, a technique, a procedure, an idea → a concept note.
  5. Not yet settled → an entry in the day’s ## Notes, until you promote it.

And the things that are none of these stay where they already go: something to do is an action on a project map; finite work with a deliverable is a project.

A benchmark result is evidence. What the benchmark taught you about the technique is a concept. The command that ran it belongs in the evidence note — and may also become a concept, if you will run it again.

The provenance rule

Evidence never depends on the current text of a concept note. An evidence note records what was done and observed inline — the exact invocation, the version, the parameters — and may link to a concept only as see also. A concept is never the origin: of an evidence note. Concepts describe your understanding now; evidence records what happened then. That separation is what lets you refine a concept freely without rewriting the past.

Substance in the day, pointers in the log

## Logs is the day’s sequence: one line per event. Substance — a derivation, a worked procedure, a page of reasoning — does not belong there, because a block of text turns a sequence into a wall. It goes in the daily note’s ## Notes section instead, one ### <heading> entry per piece, while the log gets a one-line pointer to it:

## Notes
### Woodbury identity
(A + UCV)^-1 = A^-1 - A^-1 U (C^-1 + V A^-1 U)^-1 V A^-1.

Cheap when A^-1 is already known and the update is low-rank: today's use was the
k=3 refit on [[surrogate-model]]. #concept

## Logs
- **14:32**: noted [[journal/2026/daily/2026-09-02#Woodbury identity]] ([[surrogate-model]])

cdno log note (or the note_to_daily MCP tool) writes both halves in one go: the entry under ## Notes, which it creates just above ## Logs if the day does not have one yet, and the noted [[…#Heading]] pointer line in ## Logs, followed by the entry’s own wikilinks in parentheses. So the day’s sequence shows that a derivation happened and what it touched, while the derivation itself stays out of the way. Like ## Logs, ## Notes is append-only.

Each entry’s heading is its address, so the tool holds it to a few rules: unique within the day, never a daily section name (Standup, Intention, Agenda, Meeting, Notes, Logs), no [, ], |, # or inline markup, and not starting with ^. Headings inside the entry must be level 3 or deeper. If you write an entry by hand, follow the same convention.

Mark candidates. End an entry you might reuse beyond today with the tag #concept on its last line. That is how you — or an assistant — find promotion candidates later.

## Notes is not read by the project, weekly or monthly context: those read ## Logs. An entry about a project reaches that project through the pointer line’s wikilinks and the entry’s own links (which become backlinks once the index is rebuilt; see Promotion), so a review sees that you worked something out, not the working itself.

Promotion

Write a concept note straight away when you already know you will reuse the thing. When you are not sure, note it in the day and promote it the second time it comes up.

Promotion is simply creating a concept note with origin pointing at the entries it came from. There is no separate promote operation, no counter, and no promoted to line:

cdno note create concept --title "Woodbury identity" --body-file woodbury.md \
  --origin "[[journal/2026/daily/2026-09-02#Woodbury identity]] [[journal/2026/daily/2026-09-24#Low-rank refit]]"

Creation is logged for you, to today’s daily note, as concept created [[concepts/<slug>]] — <title>, and origin records where the concept came from. Over MCP the same step is create_custom_note with body and origin.

The ## Notes entries stay as they were — the day’s record is append-only — and their daily notes have a backlink from the concept. Every write through cdno indexes the links it writes, so links from cdno log note, cdno note create or cdno note revise are in the backlinks at once.

Search before you create. A second note on a subject the library already covers is the duplicate this whole method exists to avoid. cdno search <word> --type concept first; if the concept exists, refine it rather than minting another. (Nothing refuses a second note with the same title: it is created as <slug>-2.)

An assistant connected over MCP follows the same habit. Its tool descriptions tell it to search the library before drafting an explanation, and to offer promotion when it finds #concept entries on the same subject on two or more dates.

Finding candidates, and the current limitation

Search matches words, not tags. Searching concept across daily notes finds the entries tagged #concept, but also — by stemming — any day that merely created, revised or linked a concept note, since those log lines contain concepts/. The snippet rarely shows the entry’s subject either, so open each hit and look for ## Notes entries whose last line carries #concept:

cdno search concept --type daily

A search that filters by tag is planned but not shipped.

Refinement

A concept note is mutable in place. A better explanation replaces the worse one; a correction replaces the error. The note is always the current best account — correct it rather than annotate it.

Refine through the tool, so the change leaves a trace:

cdno note revise woodbury-identity --section "Why it matters" --content-file why.md \
  --reason "added the cost comparison"

Each revision that changes the text writes one line to today’s daily log:

- **14:32**: revised [[concepts/woodbury-identity#Why it matters]] — added the cost comparison
  • The reason is required. It is the part of the history worth keeping, so say briefly why.
  • The #Section anchor is present when you revised one section, and absent for a whole-body rewrite.
  • There is no was: / now: block, unlike a project’s state change. The log records when and why; git records what.
  • Text identical to what is already there writes and logs nothing.

You can replace the whole body (--body-file, or your editor when run interactively with no file) or upsert one section (--section with --content-file). A section that exists is replaced together with its sub-sections; one that does not is appended as ## <section>. There is no append operation for a concept — refinement means rewriting what is there.

The hash guard. cdno note revise reads the note first and remembers a hash of what it read. If the file changes on disk before the revision is written — say you saved it in your editor while the revise editor was open — the revision is refused rather than overwriting that change: read the note again and redo it. Over MCP, read_note returns that content_hash and a whole-body revise_note must pass it back as expected_hash.

Edits you make directly in an editor are legitimate — the Markdown is the source of truth — but they leave no log line. That is a known limit of the history, not something cdno lint polices.

Only mutable custom types can be revised this way. Built-in types (a project’s sections belong to its own commands) and custom types declared append_only = true are refused.

No staleness

Understanding does not go stale. A theorem is as true after two untouched years as on the day you wrote it down, and an explanation you have not changed is one you were happy with. So concept notes have no verified date, no age signal, no staleness lint and no review chore. They never appear in cdno orient, the weekly or monthly context, or any review list.

The history lives in the daily log instead: every creation and revision is a dated line there, so “how has my understanding of this evolved?” is a search, not a field.

Usefulness is a question you can ask, not a metric that is kept: the note’s backlinks (which notes link to it) are one read_note away over MCP, and whether it has been mentioned in the log is a search. An unreferenced concept costs nothing and is left alone. A procedure that depends on a version can say “last run on 2026-09 against v1.4” in its own body if that matters; there is no field for it.

concepts/ is flat. A concept usually belongs to several subjects at once — a theorem is both linear algebra and optimisation — so a folder is the wrong axis. Tags carry membership, and wikilinks carry relationships.

Hub notes carry navigation. When a cluster is real, write a concept note that is mostly links — concepts/linear-algebra.md listing the theorems, the notation sheet, the techniques. It is an ordinary concept note, written by hand when you need it, never a folder decided in advance. By convention its slug is the tag it maps. It plays the part a portfolio’s _index.md plays for evidence.

If the folder grows past a few hundred files and you browse it on disk, one coarse subfolder level (concepts/maths/) is tolerated. The tool is blind to it: it never asks for a subfolder, and [[concepts/<slug>]] still resolves after you move a file by hand.

Summary

You want to…CLIMCP
Write substance into the daycdno log notenote_to_daily
Find an existing conceptcdno search <word> --type conceptsearch_notes with note_type: concept
Read a note wholecdno open concept:<slug>read_note
Create or promote a conceptcdno note create concept --origin …create_custom_note with origin
Refine a conceptcdno note reviserevise_note
List the librarycdno note list concept—

Walk one promotion end to end in Building a concept library.

Next: Vault structure.

Vault structure

A vault is a directory tree of Markdown files plus a .cuaderno/ config folder. cdno init creates the whole layout; here’s what each part holds.

vault/
├── journal/
│   └── 2026/                      # partitioned by (ISO) year
│       ├── daily/
│       │   └── 2026-04-25.md      # type: daily (append-only)
│       └── weekly/
│           └── 2026-W17.md       # type: weekly (append-only)
│
├── projects/
│   ├── surrogate-model.md        # type: project (mutable)
│   ├── _parked/                  # parked projects: complete maps that don't count
│   │   └── bayesian-opt.md
│   └── _done/
│       └── 2025/                 # closed projects: completed or dropped
│           └── thesis-chapter-2.md
│
├── actions/
│   ├── characterise-sampler.md   # type: action (manifest form)
│   └── _done/
│       └── 2026/                 # completed actions, partitioned by year
│           └── run-ablation.md
│
├── portfolios/
│   └── sparse-vs-dense-ood/
│       ├── _index.md             # type: portfolio
│       ├── 2026-03-15-chen-2025.md   # type: evidence (append-only)
│       └── 2026-04-01-ablation-b.md
│
├── stewardships/
│   ├── finances.md               # type: stewardship (flat)
│   └── health/                   # expanded variant
│       ├── _index.md             # type: stewardship
│       ├── tracking/             # type: tracking entries (append-only)
│       │   └── 2026-04-06-gym.md
│       └── routines/             # reference docs, not logs
│           └── upper-body-a.md
│
├── commitments/
│   ├── pay-rent.md               # type: commitment
│   └── _done/
│       └── 2026/                 # fulfilled commitments
│
├── questions/
│   ├── research/
│   │   └── surrogate-cost.md     # type: question (domain: research)
│   └── life/
│       └── apartment-as-home.md  # type: question (domain: life)
│
├── concepts/                     # the concept library (a custom type `cdno init` declares)
│   ├── woodbury-identity.md      # type: concept (mutable, refined in place)
│   └── linear-algebra.md         # a hub note: mostly links
│
├── inbox/                        # raw captures awaiting triage
│
└── .cuaderno/
    ├── config.toml               # vault configuration
    ├── index.db                  # SQLite index cache (auto-created, rebuildable)
    └── templates/                # note templates (override the built-ins)

Conventions worth knowing

  • _parked/ (projects) and _done/ (projects, actions, commitments) prefix folders hold inactive or finished notes. The underscore keeps them sorted out of the way and signals “not the active set.” _done/ is partitioned by year so the active folders stay scannable.
  • projects/_parked/ holds complete maps that are set aside: they take every plan write (actions, state, milestones, waiting-on), keep their hard dates in the commitments register, and do not count toward the cap. You read them at the monthly review. A handed-off project (held_by: in its frontmatter) stays in whichever folder it was in; only the name changes. See Business rules.
  • _index.md is the identity note of a folder — the portfolio note inside a portfolio folder, the stewardship note inside an expanded stewardship folder.
  • tracking/ inside a stewardship holds time-series entries; routines/ holds prescriptive reference documents (a workout plan, a checklist) — those are not logs.
  • concepts/ is flat by design: tags and links, including hand-written hub notes, give it structure. See The concept library.
  • Stewardships have two shapes: a flat stewardships/<slug>.md, or an expanded stewardships/<slug>/ folder. Only expanded ones can hold tracking entries.

.cuaderno/

  • config.toml — vault settings: the method version the vault follows ([vault] method_version), the project cap, the holder deny-list ([projects] holder_sentinels), ignore globs, template behaviour, schema extensions, variables. See Configuration.
  • index.db — a SQLite cache of the vault, used for fast search, linting, and link queries. It is rebuilt automatically when it’s stale (see Business rules); deleting it is safe. The Markdown files are always the source of truth.
  • templates/ — the note templates cdno fills when scaffolding a note. The built-ins are written here at init so you can edit them; pure variable substitution, no logic.

Next: Business rules.

Business rules

Cuaderno enforces a small set of rules that keep the method honest. Knowing them explains why some commands behave the way they do.

Markdown is the source of truth

The files on disk are canonical. The SQLite index in .cuaderno/index.db is a cache — it makes search, linting, and link lookups fast, but it can always be rebuilt from the Markdown. A stale index is recoverable; a stale file would be data loss, so the tool never lets the cache override the files. You can delete index.db at any time, or rebuild it explicitly with cdno reindex.

Startup reconciliation

On every CLI invocation, MCP session, and app launch, Cuaderno reconciles the index against the filesystem — comparing modification times and content hashes — and quietly repairs anything stale before doing your command. This is why edits you make to notes by hand (in your editor, via sync) are picked up without a manual reindex.

The five-project cap

At most five projects count at once. A project counts when it is active and names no one else as its holder: parked projects don’t count, closed ones don’t, and neither does an active project that someone else is moving (below). Creating a project at the cap creates it parked; the cap is checked whenever a project that did not count begins to: on activation, so activating a sixth stops you — you must park one first — and when you take an active project back from its holder. Parked projects live in projects/_parked/. Closed ones are not open at all: a project that is finished is completed, and one that is not going to happen is dropped, both into projects/_done/<year>/. Closing never needs a slot, and a parked project can be closed directly. The limit is configurable via config.toml (see Configuration); five is the default because it’s the point past which “active” stops meaning anything.

Parked projects are complete maps

Parking changes two things only: the project does not count, and you read it at the monthly review rather than daily. A parked map takes every plan write an active one does: next actions, Current State, milestones, waiting-on items, the core question and a holder, from the CLI and over MCP alike, so you never activate a project just to write on it. Its hard milestones and action due dates stay in the commitments register.

Only starting, switching or resuming work, and parking need an active project (cdno action start, switch, resume, and cdno project park): a project you start work on is a project you are moving, so it counts. Activate it first, parking another if the cap is full.

Handed-off projects

An open project may name who else is moving it in a held_by: line at the top of its map: held_by: "the harbour office". cdno project hand-off writes it and take-back clears it, each logging a holder on [[<slug>]] entry in today’s daily note. An active project that names a holder stays where it is, keeps its actions, its log and its dates in the register, and does not count toward the cap: the cap counts your attention, not someone else’s.

Three guards keep it honest. If you cannot name who has it, it is yours. A handed-off map says what you are waiting for in its Waiting On. And handing off is never the answer to a full cap: name a holder only when someone else really has the project. To make room, park, complete or drop one.

The deny-list. A held_by: value that names you or nobody (me, tbd, someone, ?, …) reads as no holder, so the map stays yours and counts, and the hand-off verb refuses it. Those values are a config key, [projects] holder_sentinels, with a default list you can replace, or set to [] to accept any name. Its stated limit: the tool cannot know your own name, so held_by: "<your name>" is read as someone else unless you add your name to the list. A value that is not a plain name (an unquoted wikilink, a number, a date) also reads as no holder; cdno lint reports it so you can quote it.

The method version

The vault records which version of the Research Logbook Method it follows, as [vault] method_version in .cuaderno/config.toml. That is the method’s version, not cdno’s: cdno init writes the version this cdno implements, and only cdno migrate changes it. A vault with no key is read as RLM 1.0. A cdno refuses to write to a vault on another major version (an older one: migrate it; a newer one: upgrade cdno), while reads keep working; a minor difference only prints a notice.

Append-only notes

daily, weekly, evidence, and tracking notes are append-only — Cuaderno only ever grows them, never overwrites, with one exception: when a note is renamed, references to it are corrected to the new name, even here. They are the historical record. (Projects are mutable on purpose; see next.)

Project state history is preserved

A project’s Current State is the one piece of freely-mutable prose in the vault. To keep history intact, every time you update it (via cdno project state or the MCP update_project_state tool) the previous state is auto-logged to today’s daily note before the new text overwrites it. You get a clean current view and a full audit trail in the journal. A hand-off or take-back logs the same way, as holder on [[<slug>]] with was: and now: lines.

Commitments are aggregated, not stored in one place

The commitments view is computed from four sources, so a promise is counted wherever it naturally lives:

  1. Project milestones marked with a hard deadline (--hard), of an open (active or parked) project.
  2. Stewardship periodic commitments (the recurring lines on a stewardship dashboard).
  3. Standalone commitment notes in commitments/.
  4. Action notes carrying a self-imposed due: that isn’t pinned to a milestone, of an open (active or parked) project.

A project’s own dates (sources 1 and 4) count while the project is open, active or parked: a hard date on a parked map is still a promise. Completing or dropping a project takes its milestones and action due dates out of the view until it is activated again. A standalone commitment that names a parked or closed project keeps surfacing, since it is a promise to someone else.

cdno commitments merges and sorts all four by date, with overdue items flagged.

Atomicity and its limits

Each write is captured as a transaction (file writes + index updates) that commits as a batch and rolls back on failure — while the process is alive. Startup reconciliation catches index staleness afterward, but a crash midway through a rare multi-file operation can still leave partial state on disk. In practice this is vanishingly rare; the takeaway is simply that the Markdown is authoritative and reindex + lint will surface anything odd.

Next: Contexts and energy.

Contexts and energy

Two small enumerations show up across many commands. Both are fixed vocabularies (not free text), so they stay consistent and filterable.

Contexts — the life domain

A context classifies which part of life a project, stewardship, or commitment belongs to. The set is fixed:

ContextTypical use
workYour main job
side-projectPersonal projects outside work
universityStudies, coursework, a degree
familyFamily responsibilities
householdRunning a home
legalPaperwork, contracts, official matters
personalHealth, growth, anything else personal

You set a context when you create a project (--context), stewardship (--context), or commitment (--context). It groups and colours items in views and lets the system reason about balance across your life, not just your work.

Contexts are a compile-time set, not configurable — keeping the vocabulary small and shared is the point. (Stewardships accept the same set.)

Energy — the effort a thing needs

An energy level tags how much focus an action demands, so the morning suggestion can match the work to how you actually feel:

EnergyMeaning
deepHeavy, uninterrupted focus (real thinking, hard implementation)
mediumModerate focus (routine progress, review)
lightLow focus (admin, quick wins, tidying)

You tag an action with --energy when you add it. Then:

# "I have a clear morning" — bias the suggestion toward deep work:
cdno orient --energy deep

# "I'm fried" — surface something light instead:
cdno orient --energy light

cdno orient uses the energy bias to pick which next action to suggest as your starting point. Matching the task to your state — rather than forcing the hardest thing first — is part of what makes the daily loop sustainable.

Focus

Energy says what kind of work to pick; focus says what you are on right now. It is a single slot, and it is deliberate at both ends: you put something in it, and you take it out.

To…RunLog line
put an action in the slotaction startstarted [[slug]] — text
finish itaction completeaction done on …
abandon itaction dropaction dropped on …
set it aside, to come backaction pauseaction paused on …, with next: and reason:
move to something elseaction switcha paused line then a started line
pick it up againaction resumeresumed [[slug]] — text

cdno now reads it back. Nothing is stored beside the daily log: the focus is replayed from it each time, so there is nothing to keep in sync and a line you write by hand counts as long as it has the writers’ shape.

One slot. Starting something while something else is in focus is refused, naming the way forward (switch, or pause first). You can always move on; you just say so, which makes the move a line in the log instead of a silent change of subject. The same holds on reading: a newer start displaces an older one for good, and finishing the newer one does not bring the older back.

The window. A start left open at midnight is still the focus in the morning, with its day named, and expires after that. The window is carry_over_days, default 1; set it to 0 to read today only. resume re-anchors a carried focus to today, so work you keep going on does not expire, and work you stop lets it lapse.

Pause and resume. pause is the honest way to stop without finishing: the action stays open on the map, and an optional --next records where to pick up. Pauses are remembered for paused_lookback_days, default 14, which covers a weekend. A paused action stops being offered once something later started, resumed, finished, dropped or promoted it. Promoting a bullet while it is in focus is fine: the focus follows it to the new note.

With an assistant. Over MCP the focus rides along on the reads and on the results of eighteen write tools, so an agent knows what you are on without asking. The server never refuses a write for being outside the focus. The agent’s job is to notice a change of subject and say it in one sentence, offering to capture the thought and stay, to do it now as an aside, or to move over if you say so, and to give you a cue to get back (next: is what it reads out). It raises a given topic at most once per focus, never asks you why, and never pauses or resumes without your yes.

The during: tag. While a focus is open, a log line written by cdno log, an inbox capture, and a ## Notes entry (on its noted pointer line in ## Logs) are tagged with its project: during: [[surrogate-model]] under the log line, captured_during: surrogate-model in an inbox item. The tag is what lets a later review see where captures came from, and it survives triage because the discard line copies it.

Next: Configuration.

Configuration

Vault behaviour is configured in .cuaderno/config.toml, written for you by cdno init. The defaults are sensible — you can run for a long time without touching it. This page explains what is configurable and why; the Configuration reference lists every key.

What you can configure

  • The method version. [vault] method_version records which version of the Research Logbook Method the vault follows: the method’s version, not cdno’s. cdno init writes it; you change it only with cdno migrate, never by hand. A vault without it is read as RLM 1.0.
  • The active-project cap. Change the default of five via [vault] max_active_projects. It counts the projects that are active and name no one else as their holder.
  • The holder deny-list. [projects] holder_sentinels lists the held_by: values that name you or nobody (me, tbd, someone, …), so a map carrying one stays yours and counts. Replace the list to add your own name or words in your language, or set [] to accept any name. The tool cannot know your own name: add it if you want it read as yours.
  • Ignore globs. Patterns for files the index should skip — CLAUDE.md, README.md, scratch notes — so they don’t appear in search, lint, or link checks. Patterns are additive (no negation), matched against vault-relative paths, and never delete anything on disk — they only scope what the index considers.
  • Templates. Override any built-in note template by adding a file under .cuaderno/templates/.
  • Custom note types. Declare your own types under [note_types.<name>]. cdno init declares the bundled concept type for you; a vault created before it existed gets it with cdno config note-type install --name concept (see Custom note types).
  • Schema extensions. Add vault-specific required frontmatter fields per note type (e.g. require collaborators on every project), enforced by cdno lint.

The hands-on walkthrough is Customising templates and frontmatter.

Templates

When cdno scaffolds a note, it fills a template. Templates are pure variable substitution — no conditionals, no logic. cdno init writes two starter templates (.cuaderno/templates/daily.md, and concept.md for the concept type it declares); every other built-in type uses its built-in default until you add a file for it. cdno picks the most specific template that exists:

  1. a custom variant template (for tracking, e.g. tracking-gym.md), then
  2. a custom type template (e.g. project.md), then
  3. the built-in variant default, then
  4. the built-in type default.

Because templates also define the canonical order of frontmatter keys, cdno normalise uses them to reorder hand-authored or migrated notes into a consistent shape.

Variables

Templates use {{placeholder}} markers that cdno fills from the values each note’s creation command supplies — {{title}}, {{context}}, {{created}}, and so on. The exact set available per note type, and how to use them in a custom template, is covered in Customising templates and frontmatter. An unknown placeholder is left verbatim, so use only the ones a type provides.

Custom templates can also reference static vault variables you set under [variables] in config.toml (e.g. {{author}}); these resolve on every note type, with per-type values taking precedence over a config variable of the same name.

For values that change per note, prompted variables under [variables.prompt] are gathered at creation — from a --var name=value flag, an interactive prompt, or (failing both) a clear error. The tutorial covers them in full.

Example

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

# Skip these from the index entirely (search/lint/links). Never deletes files.
ignore = ["CLAUDE.md", "README.md"]

# Require an extra field on every project note — enforced by `cdno lint`:
[schemas.project]
extra_required = ["collaborators"]

# Names that mean "me" or "nobody" in a project's `held_by:` (replaces the default list):
[projects]
holder_sentinels = ["me", "tbd", "someone", "A. Researcher"]

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

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

For the complete key-by-key reference, see Configuration reference.

That’s the concepts tour — next, put it to work in the Tutorials.

The daily loop

The core rhythm of Cuaderno is a daily loop: orient → act → log → close. None of it is mandatory or guilt-inducing; it’s a habit that keeps the vault current with almost no overhead.

Morning: orient

Start the day by asking the tool what deserves attention:

cdno orient --energy deep

orient shows commitments due soon, your active projects with their current state and top next action, and a single suggested starting point — biased toward the energy you tell it you have. Drop --energy to get a neutral suggestion; pass --energy light on a low day.

Want just the project snapshot without commitments? Use cdno status.

If you left something open yesterday, cdno now still shows it this morning, with the day named. Pick it up with cdno action resume, or set it aside with cdno action pause --next "…" and choose something else. orient itself does not list the focus or your paused work; cdno now does.

Through the day: act and log

As you work, two verbs carry most of the weight.

Log what happens — append a timestamped line to today’s journal:

cdno log "scaled the mesh to 2M cells; 4x runtime, still stable"

Capture stray thoughts without breaking flow — they land in the inbox for later triage:

cdno capture "does Chen 2025 use the same preconditioner?"

Make progress on projects as you go:

# Add the next action you just thought of:
cdno action add --project surrogate-model --title "Profile the assembly step" --energy medium

# Record where a project now stands (auto-logs the previous state to today's journal):
cdno project state --slug surrogate-model --text "Mesh scaling works; assembly is the bottleneck"

# File a useful result into the right portfolio:
cdno file --portfolio sparse-vs-dense-ood --source "ablation run B" --origin projects/surrogate-model

# Say what you are starting, so `cdno now` can answer for you later:
cdno action start --project surrogate-model --query "assembly step"

# Tick off a finished action (substring match on the bullet):
cdno action complete --project surrogate-model --query "feature set B"

When you lose the thread, ask what you were doing rather than guessing:

$ cdno now
On surrogate-model — Profile the assembly step (medium), since 09:30 (1h 30m).

There is no state behind that — it replays today’s journal and yesterday’s (the [focus] carry_over_days window in .cuaderno/config.toml), so a start made here, or by an agent over MCP all count equally, and completing, dropping or pausing the action clears it. With nothing open it says so, and shows the last thing you paused and the note you left yourself. See cdno now for the full shape, including what a start typed by hand has to look like.

When the day changes direction, say so rather than stacking one thing on another. Starting a second action while one is open is refused; the way through is a verb:

# Stopping for now, with a note to your future self:
cdno action pause --next 'pick up at "Prior approaches"'

# Moving to something else right now (a pause and a start together):
cdno action switch --project other-project --query "CI"

# Coming back; prints the note you left:
cdno action resume

Each one is a line in the journal, so the record shows when you moved and, if you said, why. When you capture a thought while something is in focus, the inbox item remembers which project you were on. See Focus.

Evening: close

A light wind-down — note anything for your stewardships and reflect:

# Log a tracked activity under a stewardship:
cdno track gym --stewardship health --content "Upper body; good energy"

# A closing reflection is just another log line:
cdno log "good focus day; pick up assembly profiling tomorrow"

That’s it. The journal now holds a faithful record of the day, your projects reflect reality, and tomorrow’s orient will pick up where you left off. Anything still in focus at midnight is carried into tomorrow, and cdno action pause is the way to put it down on purpose.

Letting Claude run the loop

Each step has an MCP equivalent (get_orientation, append_to_log, add_action, update_project_state, …), so an assistant can drive the same loop conversationally. See Connect to Claude and the example skills.

Next: Managing projects.

Managing projects

A project is a lightweight map of a piece of active work: a current state, next actions, milestones, and things you’re waiting on. You keep at most five active at once. All the verbs live under cdno project (plus cdno action for the next-action list).

Create one

cdno project create --title "Surrogate model" --context work
# -> projects/surrogate-model.md   (slug derived from the title)

--context is the life domain. Optionally link the project’s core question with --question questions/research/surrogate-cost — and if you skip it, or the question changes later, cdno project core-question --slug surrogate-model --question <target> sets it afterwards (--clear detaches). If you’re already at five active projects, the new one is created parked — activate it once you free a slot.

See where things stand

cdno project list                 # active projects + a state snippet
cdno project show surrogate-model # one project in detail
cdno status                       # all active projects + their top action

Add --json to any of these for structured output (see JSON output).

Update the current state

The Current State is the project’s one mutable paragraph — “where is this right now?”. Updating it auto-logs the previous state to today’s journal first, so you never lose the trail:

cdno project state --slug surrogate-model \
  --text "Mesh scaling works to 2M cells; assembly is now the bottleneck"

Next actions

Actions are the things to do next. By default they’re inline bullets on the project:

cdno action add --project surrogate-model --title "Profile the assembly step" --energy medium
cdno action list --project surrogate-model
cdno action complete --project surrogate-model --query "profile the assembly"

See Actions for the inline-vs-manifest distinction and promotion.

Milestones

Milestones are markers of progress. Mark one --hard to make it a real deadline that shows up in the aggregated commitments view:

cdno project milestone add --slug surrogate-model --title "Submit to ICML" --date 2026-01-22 --hard
cdno project milestone done --slug surrogate-model --query "submit to icml"

--date is optional. When a milestone is gated by a condition rather than a date, leave it off rather than inventing an estimate — a made-up date reads back later like a commitment somebody made:

cdno project milestone add --slug surrogate-model --title "All Round-1 replies received"

An undated milestone records target: TBD, stays out of the commitments view, and completes exactly like a dated one. --hard needs a real date.

Waiting-on

Track external blockers so they’re visible instead of forgotten:

cdno project waiting add --slug surrogate-model --description "Cluster quota increase from IT"
cdno project waiting resolve --slug surrogate-model --query "cluster quota"

Park and re-activate

Parking is first-class and reversible — it’s how you respect the five-project cap without deleting anything:

cdno project park --slug surrogate-model        # -> projects/_parked/, frees a slot
cdno project activate --slug surrogate-model     # bring it back (must be under the cap)

A parked map is still a complete map. Parking changes two things only: the project stops counting, and you read it at the monthly review instead of every day. You can keep writing on it exactly as on an active one, without activating it first:

cdno action add --project bayesian-opt --title "Re-read the acquisition-function survey" --energy light
cdno project state --slug bayesian-opt --text "Shelved until the ICML submission is in"
cdno project milestone add --slug bayesian-opt --title "Decide whether to restart" --date 2026-07-01

Its hard milestones and dated actions stay in cdno commitments: a deadline on a parked map is still a promise. What a parked map refuses is work: cdno action start (and switch and resume) needs an active project, because a project you are working on is one you are moving. If you find yourself working on a parked map, activate it, parking another if the cap is full.

Hand a project off

Some projects stay open while someone else is moving them: the survey is with the harbour office, the draft is with your co-author. Name the holder, and the map says so:

cdno project hand-off --slug hull-survey --to "the harbour office"
# Handed hull-survey to the harbour office (projects/hull-survey.md)

The map gains held_by: "the harbour office", today’s daily note gets an entry:

- **09:10**: holder on [[hull-survey]]
  was: (mine)
  now: the harbour office

and the project, if active, no longer counts toward the cap: cdno project list shows it with a held by: the harbour office · does not count toward the cap line. It stays where it is, keeps its actions, log and deadlines, and is still read at the weekly review, where the question is not “did it move?” but “what came back, and what is waiting on my decision?”. Say what you are waiting for:

cdno project waiting add --slug hull-survey --description "Inspection report from the harbour office"

When the work comes back to you, take it back:

cdno project take-back --slug hull-survey
# Took hull-survey back (projects/hull-survey.md)

That logs holder on [[hull-survey]] with now: (mine), and an active project counts again, so at the cap the take-back is refused with the projects that count. A parked project stays parked; taking it back only clears the name.

Three guards keep it honest:

  • If you cannot name who has it, it is yours. --to tbd, --to someone or --to me is refused (the list is [projects] holder_sentinels).
  • A hand-off is not made mid-work. While an action on the project is in focus, pause it first.
  • Handing off is never how you make room at the cap. Name a holder only when someone else really has the project. To free a slot, park, complete or drop one.

Close a project

A project ends one of two ways, and the log records which:

cdno project complete --slug surrogate-model                               # the work is done
cdno project drop --slug bayesian-opt --reason "superseded by the ICML work"  # it is not happening

Both move the map to projects/_done/<year>/, stamp closed: with today’s date and free the slot; both work on a parked project too. If anything is still open, the command stops and lists it:

surrogate-model has 2 open items:
  - [ ] Draft methods section with ablation figures (medium)
  - [ ] ICML paper submitted — hard: 2026-05-22
Complete or drop each of them (or add it to the project that now owns it), then run again.

Tick what was done, drop what is not happening (each with its own reason), and run complete again. A drop can instead let the open items go with the project: it asks first, or takes --drop-open in a script. Commitments you made to other people are never touched by either.

cdno project list --closed shows what has ended, newest first, and the MCP weekly and monthly contexts (get_weekly_context, get_monthly_context) list the projects that closed in their window. A closed project can come back with cdno project activate, or take the other outcome later if the first one was wrong.

Next: Research and evidence.

Research and evidence

This is the knowledge-building half of the method: name the questions that matter, open a portfolio for each, and file evidence into it as you go. Over months a portfolio becomes a dossier you can actually reason from.

Name a question

cdno question create --domain research --text "Does sparse attention beat dense out-of-distribution?"
# -> questions/research/does-sparse-attention-beat-dense-out-of-distribution.md

--domain is research or life. List the active ones any time:

cdno questions                  # grouped by domain
cdno questions --json | jq .

As a question’s life changes, transition it (each transition is logged to the journal):

cdno question park   --slug does-sparse-attention-beat-dense-out-of-distribution
cdno question answer --slug does-sparse-attention-beat-dense-out-of-distribution
cdno question retire --slug does-sparse-attention-beat-dense-out-of-distribution
cdno question activate --slug does-sparse-attention-beat-dense-out-of-distribution

Open a portfolio for it

A portfolio is a folder that accumulates evidence about one question:

cdno portfolio create --question "Sparse vs dense attention OOD"
# -> portfolios/sparse-vs-dense-attention-ood/_index.md

# Optionally tie it to a project when you create it:
cdno portfolio create --question "Sparse vs dense attention OOD" --project projects/surrogate-model

Already have a portfolio and want to link it after the fact? Use the retrofit verb (one of --question/--project, not both):

cdno portfolio link --portfolio sparse-vs-dense-attention-ood --project projects/surrogate-model

File evidence

Every useful artefact — a paper, an experiment result, a conversation — goes in as an evidence note. --source is the citation/reference; --origin is a wikilink to whatever produced it:

cdno file --portfolio sparse-vs-dense-attention-ood \
          --source "Chen et al. 2025, NeurIPS" \
          --origin projects/surrogate-model \
          --content "They report a 4x speedup at 95% accuracy on the OOD split."

Attaching a real file

To file a non-Markdown artefact (PDF, image, video), point --attach at it. Cuaderno copies it into the portfolio and scaffolds a linked evidence stub beside it (here --content is the abstract). Add --move to move instead of copy:

cdno file --portfolio sparse-vs-dense-attention-ood \
          --source "Chen et al. 2025" \
          --origin projects/surrogate-model \
          --attach ~/Downloads/chen2025.pdf \
          --content "Key result: 4x speedup at 95% accuracy."

Review what’s accumulated

cdno portfolio list                              # all portfolios + evidence counts + staleness
cdno portfolio show --portfolio sparse-vs-dense-attention-ood
cdno search "preconditioner" --portfolio sparse-vs-dense-attention-ood

portfolio list flags staleness — dossiers you haven’t fed in a while — which is a useful prompt during your monthly scan. Periodically synthesise the findings into the portfolio’s _index.md.

Not everything worth keeping is evidence. What a result taught you about a technique — understanding you will reuse, independent of any question — is a concept, and it lives in its own library; see The concept library for the filing test.

Next: Building a concept library.

Building a concept library

This walkthrough follows one piece of understanding from a scribble in the day to a refined concept note: you work something out, note it, meet it again three weeks later, promote it, and then improve it. The method behind each step is in The concept library.

It assumes a vault created by cdno init, which declares the concept type. For an older vault, first run cdno config note-type install --name concept. The project surrogate-model stands in for whatever you are working on.

The input files below are scratch; write them outside the vault (for example under /tmp), or a stray .md in the vault root is reported as unindexable. The commands here use /tmp.

1. Note the substance in the day

On 2 September you work out why a low-rank refit is cheap. It is not a one-line event, so it does not go in ## Logs; write it to a file (or let the editor open, below) and log it as a note:

cat > /tmp/woodbury.md <<'EOF'
(A + UCV)^-1 = A^-1 - A^-1 U (C^-1 + V A^-1 U)^-1 V A^-1.

Cheap when A^-1 is already known and the update is low-rank: today's use was the
k=3 refit on [[surrogate-model]]. #concept
EOF

cdno log note --heading "Woodbury identity" --body-file /tmp/woodbury.md
Noted journal/2026/daily/2026-09-02#Woodbury identity

The body ends with #concept because this looks reusable. Run interactively without the flags, cdno log note asks for the heading and opens the body in your editor instead.

The daily note now holds the entry under ## Notes, and a pointer to it in ## Logs, with the entry’s wikilinks copied onto the pointer line:

# Wednesday, 2 September 2026

## Notes
### Woodbury identity
(A + UCV)^-1 = A^-1 - A^-1 U (C^-1 + V A^-1 U)^-1 V A^-1.

Cheap when A^-1 is already known and the update is low-rank: today's use was the
k=3 refit on [[surrogate-model]]. #concept

## Logs
- **14:32**: noted [[journal/2026/daily/2026-09-02#Woodbury identity]] ([[surrogate-model]])

## Notes did not exist yet, so it was created just above ## Logs. You did not have to decide whether this deserves a note of its own — that decision can wait.

2. Meet it again

On 24 September the same identity comes up in a k=5 refit. Note it again, under a heading of its own for that day. --date writes to a chosen day’s note (stamped at the current time); --json shows everything the command returns:

printf 'Refit again with k=5 on [[surrogate-model]]: Woodbury keeps the update at O(k^3) rather than a full re-inverse.\n#concept\n' > /tmp/refit.md

cdno log note --heading "Low-rank refit" --body-file /tmp/refit.md --date 2026-09-24 --json
{
  "log_line": "noted [[journal/2026/daily/2026-09-24#Low-rank refit]] ([[surrogate-model]])",
  "message": "Noted journal/2026/daily/2026-09-24#Low-rank refit",
  "path": "journal/2026/daily/2026-09-24.md",
  "target": "journal/2026/daily/2026-09-24#Low-rank refit"
}

target is the entry’s address: cite it anywhere as [[<target>]].

A heading already used that day, or one that reuses a section name, is refused and nothing is written:

$ cdno log note --heading "Logs" --body-file /tmp/refit.md --date 2026-09-24
Error: heading `Logs` is not allowed in `## Notes`: it is the name of a daily section

3. Find the candidates

The subject has now come up on two dates — the moment to promote. Search the daily notes for the tag’s word:

cdno search concept --type daily
Search: concept

▎ 1. Thursday, 24 September 2026    daily
▎ journal/2026/daily/2026-09-24.md
▎ …a full re-inverse. #[concept] ## Logs - **14:40**: noted [[journal…

▎ 2. Wednesday, 2 September 2026    daily
▎ journal/2026/daily/2026-09-02.md
▎ …refit on [[surrogate-model]]. #[concept] ## Logs - **14:32**: noted [[journal…

Search matches the word concept, not the tag, so in a lived-in vault it also returns days that only created, revised or linked a concept note. Open each hit (cdno open 2026-09-24) and read the ## Notes entries that end in #concept to see which share a subject.

4. Check the library first

Before you create anything, make sure the library does not already hold this concept:

cdno search woodbury --type concept
Search: woodbury
  (no matches)

Had there been a hit, you would refine that note (step 6) rather than create a second one.

5. Promote: create with origin

Write the concept’s opening in a file — without the # Title line, which the engine writes — and create the note with origin pointing at both entries:

cat > /tmp/concept-body.md <<'EOF'
A low-rank correction to a matrix whose inverse you already hold can be inverted
without starting again.
EOF

cdno note create concept --title "Woodbury identity" --body-file /tmp/concept-body.md \
  --origin "[[journal/2026/daily/2026-09-02#Woodbury identity]] [[journal/2026/daily/2026-09-24#Low-rank refit]]"
Created concepts/woodbury-identity.md
---
type: concept
created: 2026-09-25
tags: []
origin: '[[journal/2026/daily/2026-09-02#Woodbury identity]] [[journal/2026/daily/2026-09-24#Low-rank refit]]'
---

# Woodbury identity

A low-rank correction to a matrix whose inverse you already hold can be inverted
without starting again.

## Statement

## Why it matters

## See also

The body fills the template’s {{body}} slot, and the template adds its three sections after it. origin is stored as a single quoted string. Its links are indexed with the write, so the daily notes it names show the concept among their backlinks straight away.

Fill in tags in your editor (tags: [linear-algebra]). A --field tags=… value is written as a plain string, not a list, so the tag list is easiest to edit by hand.

The creation is logged to the day you run it, and you do not log it again:

- **09:10**: concept created [[concepts/woodbury-identity]] — Woodbury identity

6. Refine it

Fill the empty sections one at a time. Each call names the section, the file holding its new text (without the heading), and a reason:

printf '(A + UCV)^-1 = A^-1 - A^-1 U (C^-1 + V A^-1 U)^-1 V A^-1\n' > /tmp/statement.md
cdno note revise woodbury-identity --section Statement --content-file /tmp/statement.md \
  --reason "stated the identity"
Revised concepts/woodbury-identity.md

With --json, the result says what was logged:

printf 'Refitting a rank-k update costs O(k^3) plus the products with A^-1, instead of a fresh O(n^3) inverse; the k=5 refit on [[surrogate-model]] is the worked case.\n' > /tmp/why.md
cdno note revise woodbury-identity --section "Why it matters" --content-file /tmp/why.md \
  --reason "added the cost comparison" --json
{
  "changed": true,
  "log_line": "revised [[concepts/woodbury-identity#Why it matters]] — added the cost comparison",
  "message": "Revised concepts/woodbury-identity.md",
  "new_hash": "1d1d1cbac3cb10a7",
  "path": "concepts/woodbury-identity.md",
  "section_target": "concepts/woodbury-identity#Why it matters"
}

Running the same revision again changes nothing, so nothing is written or logged:

$ cdno note revise woodbury-identity --section Statement --content-file /tmp/statement.md --reason "again"
No change to concepts/woodbury-identity.md

The reason is not optional. In a script, leaving it out is an error; in a terminal, you are asked for it:

$ cdno note revise woodbury-identity --no-interactive --section "See also" --content-file /tmp/see-also.md
Error: missing required flag: --reason (provide it explicitly or run interactively in a TTY)

To rewrite the whole body instead, pass --body-file, or run cdno note revise woodbury-identity in a terminal with no file and the current body opens in your editor. If the note changes on disk while that editor is open, the revision is refused rather than overwriting the change.

Only custom types that are not append-only can be revised; a project, for one, is refused:

$ cdno note revise surrogate-model --body-file /tmp/why.md --reason "tidy"
Error: note 'projects/surrogate-model.md' cannot be revised: 'project' is a built-in note type; its sections are owned by its own commands

7. Read the trail

The day’s ## Logs now tells the concept’s story in three lines, and the two earlier days keep their noted pointers:

## Logs
- **09:10**: concept created [[concepts/woodbury-identity]] — Woodbury identity
- **09:14**: revised [[concepts/woodbury-identity#Statement]] — stated the identity
- **09:21**: revised [[concepts/woodbury-identity#Why it matters]] — added the cost comparison

There is no was: / now: copy of the old text: the log records when and why, and if you keep the vault in git, git records what. Tracing how your understanding of the identity evolved is a search over the journal: cdno search woodbury --type daily.

With an assistant

Over MCP the same walk uses four tools, and the assistant’s tool descriptions carry the habits above:

  1. note_to_daily with heading and body — step 1 and 2.
  2. search_notes with query: "concept" and note_type: "daily", then read_note on each hit — step 3.
  3. search_notes with note_type: "concept", then create_custom_note with type_name: "concept", title, body and origin — steps 4 and 5.
  4. revise_note with section, content and a reason it drafts from what it changed — step 6. A whole-body revise_note needs the content_hash from read_note as expected_hash.

See Write tools and Context-gathering tools for the parameters and refusal codes.

Next: Actions.

Actions

An action is the next concrete thing to do on a project. Cuaderno treats actions as cheap by default: the normal form is a single bullet on the project map, not a note you have to create and maintain. You only give an action its own note when it grows into real investigation. All verbs live under cdno action.

Add a next action

cdno action add --project surrogate-model --title "Profile the assembly step" --energy medium

This appends a checkbox bullet to the project’s next-actions list, tagged with its energy. That’s usually all an action ever is.

List open actions

cdno action list --project surrogate-model

Bullets that have been promoted to notes show their status (active / blocked / completed / dropped) inline. --json gives the structured list.

Promote an action to a manifest note

When an action becomes an investigation that spans days and produces evidence, promote it. This rewrites the bullet as a wikilink and scaffolds an action note (with status, energy, criteria, links). The match is a case-insensitive substring of the bullet text; energy is inherited:

cdno action promote --project surrogate-model --query "profile the assembly"

You can also create an action already promoted by passing --note to add:

cdno action add --project surrogate-model \
                --title "Characterise sample efficiency across mesh sizes" \
                --energy deep --note

Start an action

Starting is optional — nothing forces you to declare it — but it is what makes cdno now able to answer “what am I in the middle of?”, which matters most on the days you come back from an interruption and cannot remember:

cdno action start --project surrogate-model --query "assembly step"

This logs started [[surrogate-model]] — Profile the assembly step (medium) to today’s journal. What gets written is the resolved bullet text, not your query, so the later completion logs matching text and the focus clears by itself.

For work that is on no map yet — the fix you noticed, the errand in front of you — --unplanned adds the bullet and starts it in one go:

cdno action start --project surrogate-model --unplanned \
    --title "Chase the licence renewal" --energy light

That is deliberately a separate flag rather than a fallback when --query matches nothing. A fallback would turn every typo into a new action, silently.

Promoting an action between starting it and closing it is fine: promotion rewrites the bullet, and cdno now follows it to the new note, keeping the original start time.

Pause, switch and resume

Only one action is in focus at a time, so starting a second while the first is open is refused, with the command that does what you meant:

$ cdno action start --project surrogate-model --query "feature set B"
Error: Profile the assembly step is already in focus on surrogate-model (since 09:30).
To move on to this instead: cdno action switch --project surrogate-model --query 'feature set B'
Or complete or pause it first.

There are three ways to leave an action without finishing it, and they say different things:

# Stopping for now; leave yourself a note on where you were.
cdno action pause --next 'pick up at the profiler output' --reason "meeting"

# Moving to something else right now; pauses the open one and starts the new one together.
cdno action switch --project surrogate-model --query "feature set B" \
    --next 'profiler output half read'

# Coming back, later today or after a night's sleep.
cdno action resume

resume prints the next: you left, and brings the action back into focus as of now. Unlike drop, a pause does not close the bullet: it stays on the map and you can still complete it later. switch is a pause immediately followed by a start, and shows in the log as exactly that. In a terminal, pause and switch ask once where to pick up; press Enter to skip.

A start left open overnight is still your focus in the morning (cdno now says since Saturday 14:05). cdno action resume makes it today’s; otherwise it lapses after a day. See Focus for the idea and action for every flag.

Two open bullets with identical text cannot be told apart by a substring query, so neither can be closed until one is edited. This is not specific to starting; action add twice does the same.

Complete an action

Completing matches a bullet by substring, ticks it off, and logs it to today’s journal. If the action had a manifest note, that note is archived to actions/_done/<year>/ and becomes append-only:

cdno action complete --project surrogate-model --query "feature set B"

Drop an action

Not everything on the list gets done. When an action is superseded, abandoned or reprioritised, drop it rather than completing it:

cdno action drop --project surrogate-model --query "demo proposal" \
    --reason "superseded by the demo-planning action"

This matches and archives exactly like complete, but the note is stamped status: dropped with no completion date, and the journal records action dropped on … instead of action done on ….

The distinction matters more than it looks. The daily log is what the weekly review, the monthly scan and every later verdict read back from — so completing work that was never performed leaves your own vault asserting something untrue, and the only repair is a correction line written by hand. A dropped action also stays out of the completed-actions views, because it carries no completion date.

--reason is optional and worth giving. “Superseded by X” and “no longer wanted” are different facts, and only one of them tells you to go looking for the replacement.

Inline vs. manifest — when to promote

Use an inline bullet (default)Promote to a manifest note
A discrete next stepAn investigation spanning multiple days
Done in one sittingProduces evidence / artefacts to link
No supporting detail neededNeeds success criteria, status, or its own notes

Keeping the default cheap is deliberate — it means capturing the next step never costs more than a sentence. See The Research Logbook Method for the rationale.

Next: Commitments and deadlines.

Renaming a note

A note’s slug is its file name, and every [[wikilink]] and every project: field types it. Sometimes the slug you got is not the one you want to live with. This tutorial renames a project whose derived slug stopped mid-title, follows what the rename changes, and covers what to do on a second machine. The reference is cdno note rename.

How you get a bad slug

The slug is derived from the title, and a long title is cut at 50 characters:

cdno project create --title "Calibrate the surrogate model against the high-fidelity thermal runs" --context work
# -> projects/calibrate-the-surrogate-model-against-the.md

Every later link now has to say [[calibrate-the-surrogate-model-against-the]]. The way to avoid this is to name the note when you create it:

cdno project create --title "Calibrate the surrogate model against the high-fidelity thermal runs" \
  --context work --slug surrogate-calibration

Every create verb takes --slug (see the slug rule), and an agent creating a note over MCP is told to propose a short one. For a note that already exists, rename it.

Preview first

Give the project an action note and start it, so that the journal and the action note both point at the project:

cdno action add --project calibrate-the-surrogate-model-against-the --title "Run the baseline sweep" --note --energy deep
cdno action start --project calibrate-the-surrogate-model-against-the --query baseline

Then preview the rename:

cdno note rename calibrate-the-surrogate-model-against-the --to surrogate-calibration --dry-run
Dry run: nothing written.
  move     projects/calibrate-the-surrogate-model-against-the.md -> projects/surrogate-calibration.md
  rewrite  actions/run-the-baseline-sweep.md                      1 field (project:)
  rewrite  journal/2026/daily/2026-11-06.md                       3 links
  alias    projects/surrogate-calibration.md gains `calibrate-the-surrogate-model-against-the`
  log      journal/2026/daily/2026-11-06.md
             renamed [[surrogate-calibration]] — Calibrate the surrogate model against the high-fidelity thermal runs
               was: calibrate-the-surrogate-model-against-the
  4 references in 2 notes · plan 2109…

Your plan hash will differ: the log lines carry their time stamps.

Each line is one change: the file moves, two other notes have references corrected (three links in the journal, from the lines for the project’s creation, the action and the start; the project: field of the action note), the project gains an alias, and today’s log gets one entry. Nothing is written. If the preview is refused, it is refused exactly as the write would be, so a dry run is a real check.

Write it

cdno note rename calibrate-the-surrogate-model-against-the --to surrogate-calibration \
  --reason "the derived slug stopped mid-title"

With the note and --to given, the command writes as soon as it has checked the plan, without a confirm, in a terminal or not; the dry run was your look, and it is advice, not a lock. Leave out --to in a terminal and you are asked for it, shown the preview and asked to confirm. That confirmed write is tied to the preview: if a note it would rewrite changed while you decided, it refuses with stale_plan and writes nothing. Over MCP the guard applies when the agent passes the preview’s plan_hash as expected_plan.

What changed

The project map has a new aliases: entry for the old name:

aliases: [calibrate-the-surrogate-model-against-the]

and the old name still works. cdno open calibrate-the-surrogate-model-against-the finds the project, and says it was found by its former name. cdno project show takes it too, and prints the current slug in its header.

Today’s log gains one entry:

- **09:44**: renamed [[surrogate-calibration]] — Calibrate the surrogate model against the high-fidelity thermal runs
  was: calibrate-the-surrogate-model-against-the
  reason: the derived slug stopped mid-title

was: is plain text on purpose. It records what the note was called, and a link there would be rewritten by the next rename. Every earlier state on [[…]] and started [[…]] line in the journal now names surrogate-calibration, so the project’s history is still one search, and a link clicked in an editor lands on the note.

This is the one place the vault edits the past. The log is append-only: what was recorded and when never changes. A reference is an address, not content, and correcting an address changes neither (see the method page). cdno lint accepts exactly that and nothing else in an archived note.

Renaming back

Rename old to new and back to old and you get aliases: [new]. The first renamed line’s link is rewritten to [[old]] like any other link to the note, and its was: old stays as written; the second reads renamed [[old]] … was: new.

When it is refused

Nothing is written on a refusal, and each has a code (the table says what to do for each). The common ones: the new slug is invalid or taken, the note is a kind that cannot be renamed (a daily note, an archived action; see which notes can be renamed), or the vault is behind its upstream.

The other machine

If the vault is a git repository synced between machines, the other one does nothing special: the renamed files, the corrected links and the alias arrive through the sync like any edit.

  • Rename on a machine that is up to date. A rename refuses with behind_upstream while the vault is behind, and --skip-sync-check overrides that only when you know nothing unpushed on the other side touches these notes.
  • If the sync stops on a conflict, resolve it, then run cdno lint. A link that came back with the old name still works through the alias, and lint warns that [[old]] names a renamed note, now new. Leaving it is harmless; cdno note rename new --relink rewrites every such link to the new name in one go (relinking the stragglers), and editing one by hand is allowed too, even in a past log entry.
  • Run any cdno command in the vault once on every machine, on a cdno that has note rename, before renaming anything an archived action references; the reference says why and what cdno lint says otherwise.

Over MCP

An agent previews with rename_note { dry_run: true }, shows you the plan, and writes with the plan’s plan_hash as expected_plan. See Renaming a note.

Commitments and deadlines

A commitment is a dated promise — to others, or a hard promise to yourself. Cuaderno keeps these distinct from your to-do list and gives you one aggregated view of everything with a deadline, wherever it lives.

The aggregated view

cdno commitments              # everything due, sorted by date, overdue flagged
cdno commitments --weeks 6    # look six weeks ahead instead of the default two

cdno commitments is a computed view. It always includes a 30-day overdue look-back on top of the lookahead window, so nothing slips silently into the past. It draws from four sources (see Business rules):

  1. Project milestones marked --hard, of an open (active or parked) project.
  2. Stewardship periodic commitments (recurring dashboard lines).
  3. Standalone commitment notes (below).
  4. Action notes with a self-imposed due: not tied to a milestone, of an open (active or parked) project.

A project’s own dates (the first and last) count while the project is open, active or parked: a hard date on a parked map is still a promise. Completing or dropping a project takes its milestones and action due dates out of the view until it is activated again. A standalone commitment that names a parked or closed project keeps surfacing, since it is a promise to someone else.

Standalone commitments

For a one-off promise that isn’t naturally a project milestone or a recurring stewardship line:

cdno commit create --title "Pay rent" --due 2026-06-01 --context personal
# -> commitments/pay-rent.md

# Optionally attribute it to a project or stewardship:
cdno commit create --title "Review Erik's draft" --due 2026-05-20 --context work --project projects/icml-paper

When it’s fulfilled, mark it done — it’s stamped and moved to commitments/_done/<year>/:

cdno commit done --slug pay-rent

When a promise ends without being kept

Not every promise is kept. The client cancels, the plan changes, the thing is overtaken by events. Marking it done would be the wrong record — cdno commitments and every weekly and monthly review read the daily log back, and a completion says the promise was fulfilled:

cdno commit drop --slug quarterly-report --reason "the client cancelled the engagement"

The note is archived beside your completed ones, stamped status: dropped with no completion date, so it never counts as finished work. --reason is optional; a typo needs no explanation, a cancellation usually deserves one.

If the date merely moved, reschedule is the verb you want — it records the move rather than ending the promise.

Nothing is destroyed by a drop: the note keeps its body and its created date. But a drop is re-decided rather than undone — there is no un-drop. If the promise comes back, make it again, and the record honestly shows both decisions.

Deadlines that live elsewhere

You often don’t need a standalone note — put the deadline where the work is:

# A hard project milestone shows up in `cdno commitments`:
cdno project milestone add --slug icml-paper --title "Camera-ready" --date 2026-02-01 --hard

# A recurring obligation on a stewardship dashboard:
cdno stewardship add-periodic --stewardship finances --title "File quarterly taxes" \
     --every "every 3 months" --next 2026-07-15

Both surface in the same aggregated list, so cdno commitments is the single place to answer “what have I promised, and when?”.

Next: Stewardships and tracking.

Stewardships and tracking

A stewardship is a small, bounded, perpetual responsibility — your health, your finances, a service you maintain. Unlike a project it never “finishes”; you just tend it. Stewardships can carry recurring commitments and, when expanded, time-series tracking. Verbs: cdno stewardship and cdno track.

Two shapes: flat and expanded

# Flat — a single dashboard file, no tracking. Good for "finances".
cdno stewardship create --name "Finances" --context household

# Expanded — a folder with room for tracking/ and routines/. Use --tracking.
cdno stewardship create --name "Health" --context personal --tracking

A flat stewardship is stewardships/<slug>.md. An expanded one is stewardships/<slug>/ with an _index.md, a tracking/ subfolder for entries, and a routines/ subfolder for reference docs (workout plans, checklists — not logs). Only expanded stewardships accept tracking entries.

See what you’re tending

cdno stewardship list                 # each one's variant, tracking count, staleness badge
cdno stewardship show --slug health

Periodic commitments

Recurring obligations attached to a stewardship show up in the aggregated commitments view:

cdno stewardship add-periodic --stewardship health --title "Dental check-up" \
     --every "every 6 months" --next 2026-09-01

--every takes a recurrence: daily, weekly, monthly, yearly, or every N months.

Tracking entries

For habits and metrics on an expanded stewardship, file a tracking note. The activity is positional and selects the template — a vault’s .cuaderno/templates/tracking-<activity>.md if you have one, else a generic fallback. (Ready-made variants live in the repo’s examples/templates/tracking/; see Customising templates.)

cdno track gym --stewardship health --content "Upper body A; RDL up to 25kg"
cdno track body --stewardship health --content "Weight 78.4kg, resting HR 54"
cdno track swim --stewardship health --content "1500m, 28min"
  • --stewardship can be omitted when there’s exactly one expanded stewardship — Cuaderno defaults to it. With more than one, it’s required.

  • --routine links a reference doc from the stewardship’s routines/ folder into the entry — but only when the resolved template has a routine: field (the gym.md example variant does; the generic default has none, so it no-ops there).

  • --content is optional; leave it empty and fill the entry’s tables in afterward.

  • --at files the entry for a day that has already passed. Recording lags the event more often than not — a statement reconciled at the weekend, a reading taken this morning and typed up tonight — and without it the entry lands on the wrong day and the trend bends.

    cdno track body --stewardship health --at 2026-04-06
    

    Filing is always journalled to today’s daily log, naming the day the entry describes, so a backfill stays visible in the record rather than quietly appearing in the past. Dates more than 50 years back or a year ahead are refused — that far out is a typo, and a typo would reshape a trend without saying so.

Numbers belong in frontmatter

A number written into the body is prose; a number in frontmatter is data. An agent files them through the MCP create_tracking_entry tool’s metrics parameter, and each key becomes a frontmatter key:

---
type: tracking
stewardship: finances
activity: savings
date: 2026-07-25
balance: 12480.50
contributed: 400.00
---

When one entry holds several comparable items — three subjects practised, four categories spent against — write a sequence of flat records rather than one key per item, so the same subject can recur within the entry:

detail:
  - { minutes: 25, subject: harmony }
  - { minutes: 20, subject: harmony }
  - { minutes: 15, subject: sight-reading }

A metric that reports a level rather than a total — a balance, a measurement, the last set of the day — reduces to the last record in the entry, and “last” means the order the records appear in the file. If you append them out of order, give every record an at field and they sort by it:

detail:
  - { balance: 1200, at: "09:00" }
  - { balance: 1240, at: "18:00" }

It is all-or-nothing: if any record lacks an at, or carries one that does not parse (09:00, 9:00, 9:00 AM and their with-seconds forms all do), the file’s own order stands and nothing is reordered. The key is at rather than time precisely because time is a plausible metric — a swim split, a lap time — and ordering a record set by one of its own measurements would report a number that was never the last reading.

Write the colon. at: 18:00 needs no quoting — the colon is what keeps YAML reading it as text — but at: 1800 is a number and never reaches the time parser at all.

Falling back to file order is the safe answer — ordering a half-stamped set would have to invent a position for the unstamped records, and whichever position it invented would quietly change which reading a last metric reports. But the fallback used to be invisible, so a set stamped morning / evening looked identical to one that sorted correctly. cdno lint now reports an at it cannot use, and a set where only some records carry one, naming the note and the value. An entry with no at anywhere is never reported: that is the normal way to write these notes.

Declaring a metric under [schemas.tracking.fields] (see Configuration) gets it type-checked on the way in — a float for a measurement or an amount, an int for a count. Anything undeclared is written as given, except the four keys that identify the note — type, stewardship, activity, date — which a metric may not name. They are the note’s identity rather than data about it, and the engine owns them: date is fixed by the filename, and activity is what every reader groups by.

Tracking entries are append-only — they’re your historical record. Read them back over a window via the MCP get_stewardship_tracking tool, or with cdno search.

From entries to series

Filing an entry is half the story. The other half — how those numbers turn into a chart, and why a series sometimes shows a number that means nothing — comes down to two things: which aggregate each metric declares, and which source, body table or frontmatter, the activity is read from. Every [tracking.<activity>] key is documented in full in the Tracking section of the configuration reference; this is the plain-language version of why those keys exist.

The metric-kind taxonomy

Every number you track is one of four kinds, and the kind decides the aggregate it wants:

KindExamplesAggregate
Totalamount spent, pages read, minutes practisedsum
Levelaccount balance, a measurement, a top setlast, or max for a high-water mark
Rate or ratinga score out of ten, perceived difficultymean
Occurrencea call, a visitnone — declare no metrics; the entry’s existence is the record

Summing is right for exactly one of these — a total — and wrong for the rest. It’s wrong for a level because it adds successive readings of one quantity instead of reporting the quantity: three statements through a spending-free month, each reading a balance of roughly 12,480, would sum to over 37,000 — a number nothing in the account corresponds to. It’s wrong for a rating because the series grows with how often you record it, not with how the session actually went — log twice a day instead of once and the total doubles for no reason but the logging cadence. An occurrence doesn’t want a number at all: declaring the activity with no metrics is a complete, valid use — the record is “this happened”, not “how much”.

Two worked examples

A scalar activity, a level. The savings activity shown above has no repeated records — every entry is one reading. balance is a level, so it wants last, not the default sum; contributed is a total, so the default is already right:

[tracking.savings]
# no `records` — this activity's metrics are scalars read straight off the entry

[tracking.savings.metrics.balance]
aggregate = "last"     # a LEVEL — the reading itself, not a running total of readings
unit      = "EUR"

[tracking.savings.metrics.contributed]
aggregate = "sum"      # a TOTAL — money added since the last entry
unit      = "EUR"

Filed against the frontmatter shown earlier (balance: 12480.50, contributed: 400.00), this produces two series: savings · balance, whose points are each entry’s balance verbatim, and savings · contributed, whose points are the sum of every contribution recorded on that date.

A record-based activity, two aggregates over the same records. practice splits by subject and tracks two different kinds of number per session:

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

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

[tracking.practice.metrics.focus]
aggregate = "mean"         # a RATING — sum would grow with how often you log, not with how focused you were

One entry covering two subjects in the same sitting:

---
type: tracking
stewardship: study
activity: practice
date: 2026-07-25
detail:
  - { minutes: 25, focus: 8, subject: harmony }
  - { minutes: 20, focus: 6, subject: harmony }
  - { minutes: 15, focus: 9, subject: sight-reading }
---

Four series come out of it, named <activity> · <group> · <metric>:

  • practice · harmony · minutes = 45 (25 + 20, summed)
  • practice · harmony · focus = 7 (the mean of 8 and 6)
  • practice · sight-reading · minutes = 15
  • practice · sight-reading · focus = 9

Same records, same date, two different reductions — because minutes and focus are different kinds of number, not because one config option was chosen for the whole activity.

Gaps, not zeros

A subject you didn’t practise on a given day produces no point on that day, not a zero. Zero-filling would draw a false line down to the axis — a session that didn’t happen is not the same as a session scored zero. The same rule means a brand-new subject needs no configuration change: the first time ear-training shows up in a detail record, its series starts there, on that date, with every date before it correctly absent rather than zero.

Which source a series comes from

An undeclared activity — no [tracking.<activity>] for it in config.toml — is read from the first table in the note’s body: one series per column, each column’s numeric cells summed. That’s right for a rep count and wrong for almost everything else, which is why the declared path above exists. If you’re still filing through a table, see Wide, not long in the example templates — a table column is a series, and a Metric / Value row shape sums unrelated numbers into one meaningless total.

Declaring an activity moves it to frontmatter: once the declaration yields a series for that activity, its body tables are no longer read — every note of it, not just the ones carrying frontmatter metrics. So the same metric can never appear twice under two disagreeing numbers, one from a table and one from the declared reduction. It also means a half-migrated activity shows only what its frontmatter carries, so migrate an activity’s notes together rather than one at a time.

Next: Weekly review.

Weekly review

The weekly review is the one slightly-longer ritual in the method — a short retrospective plus a single goal for the week ahead. It’s where you celebrate progress, notice what’s stuck, and set direction without micromanaging.

The weekly note

Each ISO week has a weekly note with four sections:

  • Wins — what moved (actions completed, evidence filed, projects advanced).
  • Challenges — what got in the way.
  • One Improvement — a single thing to change next week.
  • This Week’s Goal — the week’s anchor: one goal everything else orbits.

View the current week’s note (or any week) any time:

cdno weekly                       # this ISO week
cdno weekly --date 2026-04-20     # the week containing that date

The guided ritual

cdno review weekly walks you through it. Interactively, it prompts for each retrospective section and writes them into this week’s note, then asks for next week’s goal and sets it as the This Week’s Goal of next week’s note — so when the new week starts, its anchor is already there.

cdno review weekly

Run non-interactively (or with --no-interactive), it reads the current note rather than prompting — handy for scripts or a quick scan.

A weekly cadence that works

A simple routine, ~15 minutes:

  1. cdno status — skim active projects and their top actions.
  2. cdno commitments --weeks 2 — what’s due in the next fortnight?
  3. cdno stewardship list — any habit looking stale?
  4. cdno review weekly — capture Wins / Challenges / One Improvement, set next week’s goal.

The point isn’t a perfect record; it’s a regular moment to look up from the work, acknowledge what you did, and choose one direction. Parking, completing or dropping a project here, or retiring a question, is a good outcome, not a failure.

A question per relationship

Not every open project is asked the same thing, because not every project is yours to move. Walk the active projects (cdno project list shows which name a holder) and ask each the question its relationship implies:

  • Yours (no holder): did it move? If it sat still, is it still the priority, or should it wait? Parking it is a fine answer.
  • Handed off, and its map changed this week: what came back, and what is waiting on my decision? Not “did it move?”: the moving is someone else’s.
  • Handed off over an empty Waiting On: a name over an empty Waiting On is a parked map wearing a name. Park it, or take it back.
  • Handed off, but you worked on it yourself on two or more days this week: doing a thing on a handed-off map does not make it yours, doing most of its work does. Take it back, or change the name to whoever really has it.
  • Handed off, and stuck for weeks (at the monthly review, from its list of stuck projects): chase the holder, or take it back.

A handed-off project that did not change this week needs no question at all. The questions are prompts, never verdicts, and a hand-off is never an answer to a full cap: to make room, park, complete or drop a project. See Managing projects.

With Claude

The MCP tools get_weekly_context, read_weekly_note, and upsert_weekly_section let an assistant run the same review conversationally — reading your week back to you and writing the sections as you talk. get_weekly_context carries, per active project, counts, held_by, changed_this_week, waiting_on_empty and focus_days_while_held, so an assistant can ask each project the question above (the stuck-project question reads get_monthly_context’s stuck_projects, not these fields); the weekly-review example skill shows how. See Connect to Claude.

Next: Inbox and triage.

Inbox and triage

The inbox is a pressure-release valve: capture a thought now, decide what to do with it later. This keeps the daily loop friction-free — you never have to stop and classify something mid-flow.

Capture

cdno capture "does Chen 2025 use the same preconditioner?"
cdno capture "ask IT about the cluster quota"

Each capture becomes a small slug-named note in inbox/. That’s the whole cost — no fields, no decisions. Capture liberally.

Triage

When you have a moment (a natural fit for the weekly review), process the inbox:

cdno triage

Interactively, triage walks each pending capture and offers, for each one, to:

  • keep it as a project action — turn it into a next action on a project, or
  • discard it — it served its purpose, or
  • skip it — leave it for next time.

Run non-interactively (or with --no-interactive), triage just lists what’s pending — a quick way to see the backlog without acting on it.

A healthy inbox is empty-ish

The inbox is a transit point, not a filing cabinet. The goal isn’t zero at all times — it’s that nothing important lives only there. Anything worth keeping becomes an action, a piece of evidence, a question, or a log line; the rest gets discarded without ceremony.

With Claude

The MCP tools capture, triage_inbox (lists pending items), and discard_inbox_item let an assistant capture on your behalf and help you clear the backlog conversationally.

Next: Searching your vault.

Searching your vault

Cuaderno keeps a full-text index of every note, so you can find anything fast — ranked best-first, with filters by type, date, and portfolio. The command is cdno search.

The index covers each note’s title as well as its body, and a title match counts for ten times a body match. So searching for words you remember from a note’s heading surfaces that note first, even if a dozen other notes mention the same words in passing — there is no separate “search by title” command to remember.

If you already know exactly which note you want, cdno open takes you straight there by slug, date, or path, without ranking anything.

In a terminal, cdno search also follows its results with a picker: choose a hit and it opens in your editor. The results print first either way, so piping or redirecting the output is unaffected.

cdno search "preconditioner"
cdno search "sparse attention"     # multiple words are ANDed

The query is matched case-insensitively and terms are combined with AND. Quotes and operators are treated as literal words, not search syntax. Results come back ranked, best match first.

Filter the results

# Only one note type:
cdno search "ablation" --type evidence

# Within a date window (inclusive):
cdno search "mesh" --from 2026-03-01 --to 2026-03-31

# Only inside one portfolio:
cdno search "speedup" --portfolio sparse-vs-dense-attention-ood

# Cap the number of hits (default 20):
cdno search "todo" --limit 5

Filters combine, so you can scope tightly:

cdno search "preconditioner" --type evidence --portfolio sparse-vs-dense-attention-ood --from 2026-01-01

Scripting with --json

--json returns the ranked hits as a JSON array — each with path, note_type, title, snippet, and score — ready for jq or another tool:

cdno search "speedup" --json | jq -r '.[].path'

See JSON output for the exact shape.

When search feels stale

Search reads the SQLite index, which is reconciled automatically on every run. If results ever look out of date (e.g. after a bulk external edit), rebuild it explicitly:

cdno reindex

The Markdown files are always the source of truth; the index is just a rebuildable cache (see Business rules).

Next: Customising templates and frontmatter — tailor how notes are scaffolded and which frontmatter fields they require.

For exhaustive detail on any command, see the CLI reference.

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

Tracking people

Cuaderno’s built-in types don’t include a “person” — deliberately, since a person decomposes into the notes you already keep (daily logs, meetings, actions, commitments). But if you want to answer questions like:

  • What was my last interaction with Jane?
  • When did I ask Jane to do something — and what did she ask me?

…a custom note type plus a linking convention gets you there, with no bespoke CRM. This recipe is the worked example; the files live in examples/note-types/person/.

1. Declare the type

Add to .cuaderno/config.toml:

[note_types.person]
folder = "people"

That’s the minimum — a person’s identity is the note title. (Add required/optional fields if you want enforced frontmatter; see the reference.)

Optionally copy the example person.md to .cuaderno/templates/person.md for a note with a ## Log section. Without a template, Cuaderno synthesises a minimal note.

2. Create people

cdno note create person --title "Jane Smith"

This writes people/jane-smith.md. cdno note list person enumerates them; cdno lint keeps them honest.

The key habit: whenever a person shows up in a daily log, a meeting section, or an action, reference them with a [[people/<slug>]] wikilink:

## Logs
- **14:30**: standup — [[people/jane-smith]] asked me to review the sparse-attention draft

Body wikilinks are indexed, so every mention becomes a backlink on the person’s note, and the raw text is full-text searchable.

4. Answer the questions

“What was my last interaction with Jane?” — the reliable answer is the top line of her person note’s ## Log, which you keep most-recent-first. To scan across the vault, search her slug:

cdno search "people/jane-smith" --type daily --from 2026-06-01

Search ranks by relevance, not date, so bound the window with --from / --to and read the dates on the hits (daily notes carry their date in the filename) rather than trusting the order. Drop --type to include meetings and action notes.

“What did Jane ask me — and what did I ask her?” — this is direction, which lives in your prose, not in structure. Write the log line so the direction is explicit (“Jane asked me…”, “I asked Jane…”), and either keep a running ## Log in her person note or let search surface the lines. A Claude skill can read the dated results and summarise who-owes-what; the structure gives it the material, the prose gives it the direction.

Why a “person” type and not a built-in

A custom person type is schema-only — it gives you a folder, linting, search, and backlinks, but no bespoke behaviour. That’s exactly right here: people don’t need caps, state machines, or aggregation; they need to be findable and linkable. If you later want richer per-person structure, add fields to the config declaration — no code change. See Custom note types for the full schema.

CLI reference

cdno is the command-line interface to a Cuaderno vault. This section documents every command. For the why behind them, see Concepts and Tutorials.

cdno [OPTIONS] <COMMAND>

Run cdno --help for the command list, or cdno <command> --help for any command’s flags.

Global options

These apply to every command:

FlagEffect
--vault <PATH>Operate on the vault at PATH. Overrides both discovery and CUADERNO_VAULT_PATH.
--jsonEmit machine-readable JSON instead of a formatted table (see below).
--no-interactiveNever prompt; a missing required argument is an error. Implicit unless both stdin and stdout are TTYs. Also turns off interactive reports.
--color <WHEN>auto (default), always, or never. See Colour and interactivity. Spelled --colour too.
-h, --helpPrint help.
-V, --versionPrint the version (top level only).

Finding the vault

When --vault is not given, cdno discovers the vault by walking up from the current directory until it finds a .cuaderno/ folder. If that finds nothing, it falls back to the CUADERNO_VAULT_PATH environment variable. Standing inside a vault always wins over the env var. See Initialise a vault.

Interactive vs. scripted

Write commands follow one convention (the “flags-and-prompts” pattern):

  • Interactive terminal, missing a required flag → cdno prompts for it, then asks you to confirm before writing.
  • Non-interactive (either stream not a terminal — piped, redirected, < /dev/null, or running in CI, which is the usual reason neither is — or --no-interactive) → a missing required flag is an error. Supply every flag and the command runs unattended.

This means the same command serves a human at a prompt and a script with no changes.

Read commands add one thing on top. In a terminal, several listings offer to open one of the rows they just printed — cdno project list asks which project you want to see, prints it, and asks again until you press Esc. The listing itself is printed first either way, so piping, --no-interactive, and --json all behave exactly as they did before. See Colour and interactivity.

JSON output

--json makes any supported verb emit structured output:

  • Read verbs (commitments, questions, status, now, orient, search, open, and the list/show verbs of project/portfolio/stewardship, plus action list) emit their listing or detail object.
  • Write verbs (log, capture, file, track, and the create/update verbs of project, action, portfolio, stewardship, question, commit) emit a { "path": ..., "message": ... } result, and run non-interactively (so prompts can’t corrupt the JSON on a terminal). project complete and drop add what was let go, and print a refusal object and exit 1 while items are open (see JSON output).
  • migrate emits its report, the plan with --dry-run (see migrate).
  • Maintenance / interactive / bootstrap commands (init, lint, reindex, normalise, triage, review, weekly, monthly) ignore --json.

The CLI’s JSON shapes match the MCP server DTOs. See JSON output for every shape.

Naming a note: --slug

Every verb that creates a note derives its filename, the slug, from the title. For a long title the derived slug can stop mid-thought (rerun-the-surrogate-model-ablation-on), and it is what every later [[wikilink]] has to type. The create verbs therefore take an optional --slug <SLUG> to name the file yourself: project create, question create, portfolio create, action add --note, action promote, note create, stewardship create and commit create. The title is unchanged; only the filename is.

An explicit slug must be:

  • lowercase a-z, 0-9 and single -, with no - at either end;
  • at most 50 characters;
  • not something YAML would read as a number, a boolean, a null or a date (2026, yes, null, 2026-11-05): add a word to it.

An explicit slug is used exactly or refused, never rewritten. If another note already holds the name (as its filename, or as the folder of a folder note such as portfolios/surrogate-model/) the create is refused and nothing is written; pick another name. The -2 suffix belongs to derived slugs only. --slug is never prompted for; omit it and the slug derives from the title. In a terminal a refusal reads:

$ cdno project create --title "Surrogate model" --context work --slug surrogate-model
Error: creating project

Caused by:
    slug 'surrogate-model' is taken by 'projects/surrogate-model.md' (as its stem)

Under --json a refusal is a coded object.

A derived slug takes -2 when the name is held, and a folder note’s name counts as held: a question titled “Surrogate model” created beside the portfolio portfolios/surrogate-model/ gets surrogate-model-2, since the bare name would shadow the portfolio’s _index in a [[surrogate-model]] link. This applies to the verbs that disambiguate: project create, question create, action add --note and action promote, note create and commit create. portfolio create and stewardship create keep the raw derived slug (a portfolio’s slug correlates with its question’s) and refuse a collision outright.

cdno action add --project surrogate-model --note --energy deep \
  --title "Rerun the surrogate model ablation on the held-out split with feature set B" \
  --slug feature-set-b-ablation
# -> actions/feature-set-b-ablation.md

Renaming a note after the fact is a separate verb, planned under RFC 0007.

The commands

CommandWhat it does
initCreate a new vault
logAppend a line to today’s daily note, or (log note) a worked-out entry under ## Notes
captureDrop a quick note into the inbox
triageProcess inbox captures
orientMorning orientation
statusActive projects + top actions
nowWhat you are in the middle of
openResolve a note reference to its path
weeklyShow the weekly note
monthlyShow the monthly note
commitmentsAggregated deadlines
questionsList active questions
searchFull-text search over titles and bodies
reviewGuided weekly/monthly review
projectManage project maps
actionManage next actions
portfolioManage evidence portfolios
fileFile evidence into a portfolio
questionManage question notes
stewardshipManage stewardships
trackFile a tracking entry
commitManage standalone commitments
lintValidate the vault
reindexRebuild the index
migrateRecord this cdno’s method version in the vault
normaliseReorder frontmatter
completionsShell-completion scripts

cdno init

Create a new vault: the folder tree, a default .cuaderno/config.toml (which declares the concept custom type), and starter daily.md and concept.md templates (every other built-in type uses an in-binary default until you eject one).

cdno init [OPTIONS] [PATH]

Arguments

ArgumentDescription
[PATH]Target directory. Defaults to the current working directory.

Options

Only the global options apply. init ignores --json.

Examples

# Create a vault in a new directory:
cdno init ~/notebook

# Initialise the current directory as a vault:
cdno init

It fails if the target already contains a .cuaderno/ directory (it won’t clobber an existing vault).

See also

cdno log

Append a log entry to today’s daily note (creating the note if it doesn’t exist yet).

cdno log [OPTIONS] <MESSAGE>
cdno log note [OPTIONS]

Arguments

ArgumentDescription
<MESSAGE>The log message. Quote it if it contains spaces.

Options

FlagDescription
--at <TIMESTAMP>Override the timestamp. Accepts YYYY-MM-DDTHH:MM:SS or YYYY-MM-DDTHH:MM. Defaults to now.

Plus the global options. With --json, emits a {path, message} result.

Examples

cdno log "scaled the mesh to 2M cells; 4x runtime, still stable"

# Backdate an entry:
cdno log "forgot to record: fixed the sampler seed" --at 2026-04-24T18:30

# Scripted:
cdno log "nightly run complete" --json
# -> { "path": "journal/2026/daily/2026-04-25.md", "message": "Logged to ..." }

Daily notes are append-only — log only ever adds.

A first argument, before any option, that names a subcommand (note) selects it. To log that bare word as a message, pass it after --: cdno log -- note.

cdno log note

Write worked-out substance (a derivation, a procedure, a page of reasoning) to a daily note as one ### <heading> entry under ## Notes. The pointer line noted [[journal/<year>/daily/<date>#<heading>]] is written to ## Logs for you in the same write, followed by the body’s wikilinks in parentheses when it has any. Keep ## Logs for one-line events and put the substance here.

cdno log note [--heading <STRING>] [--body-file <PATH>] [--date <YYYY-MM-DD>]
FlagDescription
--heading <STRING>The entry’s heading, written as ### <heading> and used as the pointer’s anchor. Prompted in an interactive session.
--body-file <PATH>A file holding the entry’s body. Leading blank lines and trailing whitespace are dropped; the first line’s indentation is kept, so an entry may open with an indented code block. A blank body is refused. Opened in your editor in an interactive session.
--date <YYYY-MM-DD>The day whose note receives the entry, stamped at the current time. Defaults to today.

Without --heading or --body-file, an interactive session prompts for the heading and then opens the body in your editor, and confirms before writing; under --no-interactive (or --json) each is a missing-flag error.

The heading must be unique within the day, must not reuse a daily section name (Standup, Intention, Agenda, Meeting, Notes, Logs), and must not contain [, ], |, # or inline markup (bold, italics, code) nor start with ^. Headings inside the body must be level 3 or deeper and are held to the same uniqueness rule. A refused entry writes nothing.

End an entry that could be reused beyond today with the tag #concept on the body’s last line, so the review can find it as a candidate for promotion to a concept note.

The command prints Noted <target>, where the target is the entry’s anchored link (journal/…#<heading>). With --json it emits path, message, target and log_line:

cdno log note --heading "Woodbury identity" --body-file woodbury.md --json
# -> { "path": "journal/2026/daily/2026-04-25.md",
#      "message": "Noted journal/2026/daily/2026-04-25#Woodbury identity",
#      "target": "journal/2026/daily/2026-04-25#Woodbury identity",
#      "log_line": "noted [[journal/2026/daily/2026-04-25#Woodbury identity]] ([[projects/surrogate-model]])" }

Cite the entry elsewhere as [[<target>]].

Focus tag

When an action is in focus, a carried one included, cdno log and cdno log note append an indented continuation naming its project to the line they write, so a later review can see where a thought came from:

- **11:12**: checked the mesh
  during: [[surrogate-model]]

With nothing in focus the line is written as given. If the focus window cannot be read the line is written untagged rather than the command failing. The tag sits below the entry’s head, so it never affects how cdno now reads the log.

append_to_log — the same operation for AI clients; note_to_daily is the counterpart of cdno log note.

See also

cdno capture

Drop a quick note into inbox/ with a slug-based filename, to be processed later with triage.

cdno capture [OPTIONS] <TEXT>

Arguments

ArgumentDescription
<TEXT>The note text. Quote it if it contains spaces.

Options

Only the global options. With --json, emits a {path, message} result.

Examples

cdno capture "does Chen 2025 use the same preconditioner?"
cdno capture "ask IT about the cluster quota" --json

When an action is in focus, the new inbox item’s frontmatter gets captured_during: <project-slug>, recording where your attention was when the thought arrived. It is the one thing capture adds on its own; with nothing in focus the item is exactly what you typed.

Capture is meant to be frictionless — no fields, no decisions. Classify later during triage.

capture.

See also

cdno triage

Process uncategorised inbox/ captures. For each one, keep it as a project action, discard it, or skip it.

cdno triage [OPTIONS]

Options

Only the global options. triage ignores --json.

Behaviour

  • Interactive — walks each pending capture and prompts you to keep (turn into a project action), discard, or skip.
  • Non-interactive (piped or --no-interactive) — just lists what’s pending, without changing anything.

A capture’s captured_during tag outlives the file. When triage clears an item, whether you discarded it or kept it as an action, the log line it writes (triaged inbox item … -- discarded: …) carries the item’s tag as a during: [[<slug>]] continuation, copied from the item rather than from whatever is in focus at triage time.

Examples

# Work through the inbox:
cdno triage

# Just see the backlog (no changes):
cdno triage --no-interactive

triage_inbox (lists pending items) and discard_inbox_item (clears one).

See also

cdno orient

Daily orientation: the commitments due soon, your active projects, a single suggested starting point, and any stewardship habits that have lapsed.

A habit counts as lapsed when its line in a stewardship dashboard’s ## Active Habits section declares it so — a status starting with “lapsed” after the em-dash, e.g. - Swimming 1x/week — lapsed since March. The dashboard is the source of truth (updated during reviews); orientation only surfaces what it says, without judgement.

cdno orient [OPTIONS]

The suggested start is drawn from the projects that count toward the cap first (active, and naming no one else as holder; parked projects are not listed at all): a project handed to someone else (cdno project hand-off) is not your move, so it is suggested only when no project of yours has an open action, and then with its holder named. Its card in the project list carries a held by: line.

Projects that share a core question are one undertaking, so two or more of them read as one card: the question as its title, then one line naming each project with its next action (held by <name> beside one someone else is moving). A project with no question, or the only one under its question, keeps its own card. Under --json the same arrangement is the groups array (see JSON output).

In a terminal this then offers to open one of the projects it just listed, printing what cdno project show would and asking again until you press Esc. Piped output, --no-interactive, and --json skip the prompt. See Colour and interactivity.

orient does not show what is in focus or the last thing you paused, and neither field is in its --json: run cdno now for those. The MCP get_orientation does carry both, because an agent has no other cheap way to know.

Options

FlagDescription
--energy <ENERGY>Bias the suggested starting point toward this energy level: deep, medium, or light.

Plus the global options. With --json, emits the orientation as a structured object.

Examples

cdno orient                 # neutral suggestion
cdno orient --energy deep   # bias toward a heavy-focus action
cdno orient --energy light  # bias toward something quick on a low day
cdno orient --json | jq '.slots'   # active, counted and cap

get_orientation.

See also

cdno status

A quick snapshot: your active projects and each one’s top next action. Lighter than orient — no commitments digest, no suggestion.

cdno status [OPTIONS]

In a terminal this then offers to open one of the projects it just listed, printing what cdno project show would and asking again until you press Esc. Piped output, --no-interactive, and --json skip the prompt. See Colour and interactivity.

Options

Only the global options. With --json, emits the snapshot as structured data.

Examples

cdno status
cdno status --json | jq '.[].slug'

list_projects — the projects view for AI clients.

See also

cdno now

What you are in the middle of: the action most recently started, unless it has since been completed, dropped or paused. Focus is one slot: starting something new displaces the old start, which never comes back.

cdno now [OPTIONS]

There is no state behind this. It replays the ## Logs of today and of the [focus] carry_over_days days before it (default 1, so a start left open yesterday is still your focus this morning), so a start made from cdno action start, from an agent over MCP, or typed into the daily note by hand all count — and a completion, a drop or a pause clears it. Nothing to keep in sync, and nothing to go stale.

cdno action promote rewrites the bullet it matched, and the focus follows it: the action promoted on line it logs renames the open start to the new note, with the original start time, so action complete and action drop still close it.

A line you type yourself has to match the shape the writers emit:

- **09:30**: started [[surrogate-model]] — Draft the methods section (deep)

Both halves are required. The - **HH:MM**: stamp is what makes the line a log entry at all, and the separator is an em dash (U+2014), not a hyphen. The parser requires that exact codepoint, so that ordinary prose beginning “started something” is never mistaken for a focus. That strictness means a line missing the stamp, or typed with -, is simply not picked up here — but cdno lint reports it, naming the line and the likely cause, so a near-miss does not stay invisible.

$ cdno now
On surrogate-model — Draft the methods section (deep), since 09:30 (1h 30m).

cdno now has three shapes.

Something is in focus. The line above. A start carried over from an earlier day names it (since Saturday 14:05 (21h 7m)), and the elapsed time counts across midnight.

A carried focus that was picked up again. A focus carried over from an earlier day and re-anchored with action resume reads the new time first and keeps the original:

$ cdno now
On surrogate-model — Draft the methods section (deep), picked up 08:50 today (started Saturday 14:05).

Resuming a pause shows the plain since HH:MM shape, because the pause had already closed the earlier start.

A day more than six days back is shown as a date (2026-09-22 14:05), since a weekday name would be ambiguous.

Nothing in focus. It says so, and shows the most recent pause that nothing has picked up since, with its re-entry hint:

$ cdno now
Nothing started.
Last paused: surrogate-model — Draft the methods section (10:40), next: pick up at "Prior approaches"

The pause is looked for in the last paused_lookback_days days (default 14), so a Friday pause is still there on Monday. Pick it up with cdno action resume.

--line

--line prints one sanitised line of at most 160 characters for a prompt segment or hook:

Focus: surrogate-model — Draft the methods section (since 09:30)
Focus: none (last paused: surrogate-model — Draft the methods section, next: pick up at "Prior approaches")

Control characters are stripped and an over-long line is cut. It prints nothing and exits 0 when no vault is found or anything goes wrong, so it can sit in a prompt without ever breaking it. With --json as well, --line wins. The Claude Code hook example runs it before every prompt.

--json

KeyMeaning
project, actionThe focus’s project slug and its bullet text exactly as logged (energy suffix and all).
titleThe bullet text without link and energy suffix, for display. For an attached note it is the last segment of the note’s slug.
noteThe attached action note’s path (actions/<slug>), or null for an inline bullet.
energydeep, medium or light.
startedHH:MM of the open marker. Kept for compatibility.
started_at, dateThe marker’s full datetime (2026-10-04T09:30) and its day.
carriedtrue when date is not today.
origin{started_at} of the earlier start a resume continues, else null.
elapsed_minutesMinutes since started_at, across midnight.
last_pausedThe most recent unresolved pause: project, action, title, at, next, reason. Present whether or not something is in focus; null when there is none.

Every key is always there and is null when it has no value (all of them but last_paused when nothing is in focus), so a caller can test one field without first branching on the shape of the document.

The action field is the bullet text exactly as logged, energy suffix and all. That is the same string cdno action complete matches, which is why the pairing works.

current_focus — the same read, for an agent.

See also

cdno open

Open a note in your editor. Where cdno search answers “where did I write about this”, open answers “take me to the note I mean” — it is addressing, not searching.

cdno open [OPTIONS] [REFERENCE]

Run it with no reference and it offers a picker over every note, most-recently-edited first.

Arguments

ArgumentDescription
[REFERENCE]The note to resolve. Omit it and pass --list to see every note. Tab-completion offers your vault’s slugs.

A reference can be any of:

FormExampleResolves to
Bare slugsurrogate-modelthe note with that slug, whatever its type
Type-scoped slugproject:surrogate-modelthat slug within one note type
Calendar wordtoday, yesterday, tomorrowthe daily note for that day
Date2026-08-21that day’s daily note
ISO week2026-W34that week’s weekly note
Month2026-08that month’s monthly note
Vault-relative pathjournal/2026/daily/2026-08-21.mdthat file
Path without .md, as a wikilink spells itconcepts/fooconcepts/foo.md
Absolute path inside the vault/home/you/vault/projects/foo.mdthat file

Portfolios and expanded stewardships are addressed by their folder name, not the literal _index: cdno open surrogate-model reaches portfolios/surrogate-model/_index.md.

The calendar words always mean the journal. A note genuinely named today stays reachable as <type>:today.

Options

FlagDescription
--pathPrint the note’s absolute path instead of opening it.
--listPrint every note as path<TAB>title<TAB>type, for piping to a fuzzy finder. Opens nothing and takes no reference.
--editor <COMMAND>Editor command template for this invocation. Outranks $CUADERNO_EDITOR, $VISUAL, and $EDITOR.

Plus the global options. With --json, a resolved reference emits {"path": …} and --list emits an array of {path, title, type}.

When stdout is not a terminal, cdno open prints the path instead of launching anything. So cdno open today | cat, a script, --no-interactive, and --json all behave the same way, and no editor is ever started into a pipe.

Choosing the editor

First one set wins:

--editor <COMMAND>this invocation only
$CUADERNO_EDITORyour shell
$VISUAL, then $EDITORyour shell
(nothing set)the operating system’s default handler for .md
export CUADERNO_EDITOR='code -g {path}'

{path} marks where the note’s path goes. Leave it out and the path is appended, so a bare nvim works. Quoting is honoured, so a program name may contain spaces:

export CUADERNO_EDITOR='"/Applications/Sublime Text.app/Contents/SharedSupport/bin/subl" -w {path}'

A value whose first word contains :// is handed to the operating system instead of executed, with the path percent-encoded — obsidian://open?path={path}.

cdno waits for the editor to exit. It does not try to guess whether your editor is a terminal or a GUI one — code -w blocks and code does not, and a guess would be wrong in a way you could not override. Waiting is correct for every terminal editor and harmless for a GUI one that returns immediately. If the editor exits non-zero, cdno open exits with the same code, the way git commit treats an abandoned edit.

There is no per-vault editor setting, on purpose

You might expect .cuaderno/config.toml to carry this — a research vault that opens in Obsidian, a code vault that opens in your editor. It deliberately does not.

A vault is a git repository, and --vault exists so cdno can be pointed at one you did not create. A setting that names a program to run cannot live in data that gets cloned and synced: opening a note in someone else’s vault would run their choice of program on your machine. It is the same reason git does not honour every setting from a repository you just cloned.

Restricting such a setting to a bare binary name would not help. sh is a binary name, and sh <the note> executes the note’s own contents — which, in a vault you cloned, the author also wrote. The fix is that the setting comes from your shell rather than from the data.

If you work across vaults that want different editors, set CUADERNO_EDITOR in a per-directory shell hook (direnv, or your shell’s chpwd), which keeps the decision on your machine.

Archived notes

Opening a note under actions/_done/ prints a warning first: its text was frozen when it was archived, and cdno lint reports an edit to the existing text as an error. Appending to it is fine. The warning does not stop you — markdown remains the source of truth.

When a reference does not resolve

Two cases, and they say different things:

  • Nothing matched. The error names the closest few notes by their type-scoped form — so a mistyped slug tells you what you probably meant rather than dumping the vault.
  • The slug matched more than one note. cdno open never guesses, because opening the wrong note is a mistake you only discover after typing into it. The error names each type-scoped form instead, and any of them resolves. This happens when a stewardship and a portfolio share a name, for instance — stewardships/gym.md and portfolios/gym/_index.md are both gym.

A reference that looks like a path never falls back to fuzzy matching: a typo there means “no such file”, not a near-miss opened on your behalf.

Examples

cdno open                             # pick from every note
cdno open today                       # today's daily note
cdno open surrogate-model             # by slug
cdno open project:surrogate-model     # when one slug is used by two types
cdno open 2026-W34                    # that week's weekly note
cdno open --path today                # print the path, open nothing
cdno open --list | head               # every note, tab-separated
cdno open today --json | jq -r .path
cdno open notes --editor 'code -g {path}'

With a fuzzy finder

cdno open has its own picker, so you do not need fzf at all. If you would rather use yours — your keybindings, your preview window — --list gives it candidates. Because --list emits vault-relative paths and open accepts absolute ones, the round-trip works from any directory, with no need to cd into the vault first:

cdno open "$(cdno open --list | fzf --with-nth=2.. --delimiter='\t' | cut -f1)"

Worth a shell function:

cdo() {
  local pick
  pick=$(cdno open --list | fzf --with-nth=2.. --delimiter='\t' | cut -f1) || return
  [ -n "$pick" ] && cdno open "$pick"
}

This beats running fzf over the vault directory directly, because the candidates carry each note’s title rather than only its filename — filenames here are slugs, so a note titled “Surrogate model” would otherwise never match the words you remember. The listing also respects the vault’s ignore globs.

See also

cdno weekly

Show the weekly review/plan note: Wins, Challenges, One Improvement, and This Week’s Goal.

cdno weekly [OPTIONS]

Options

FlagDescription
--date <DATE>Any day in the target ISO week (YYYY-MM-DD). Defaults to this week.

Plus the global options. weekly ignores --json.

Examples

cdno weekly                     # this ISO week
cdno weekly --date 2026-04-20   # the week containing 20 Apr 2026

To write the weekly sections rather than just view them, use the guided review weekly.

read_weekly_note and get_weekly_context.

See also

cdno monthly

Show the monthly review note: Wins, Themes, Next Month’s Focus, and the month’s linked weeks.

cdno monthly [OPTIONS]

Options

FlagDescription
--date <DATE>Any day in the target calendar month (YYYY-MM-DD). Defaults to this month.

Plus the global options. monthly ignores --json.

Examples

cdno monthly                     # this calendar month
cdno monthly --date 2026-04-20   # the month containing 20 Apr 2026

To write the monthly sections rather than just view them, use the guided review monthly.

read_monthly_note and get_monthly_context.

See also

cdno commitments

List aggregated commitments across the vault — the hard milestones of active projects, standalone commitment notes, stewardship periodic commitments, and self-imposed action-note deadlines — sorted by date, with overdue items flagged.

cdno commitments [OPTIONS]

Options

FlagDescription
--weeks <WEEKS>Lookahead window, in weeks. Default 2. A standing 30-day overdue look-back always applies on top.

Plus the global options. With --json, emits the list as structured data.

Examples

cdno commitments              # next 2 weeks + anything overdue
cdno commitments --weeks 6    # next 6 weeks
cdno commitments --json | jq '.[] | select(.overdue)'

How it’s computed

This is a derived view, not a single file. It merges four sources (see Business rules): project milestones marked --hard, stewardship periodic commitments, standalone commit notes, and action notes with a self-imposed due:. A project’s own dates count while the project is open, active or parked: a hard date on a parked map is still a promise. Completing or dropping a project takes its milestones and action due dates out of the view until it is activated again. A standalone commitment that names a parked or closed project keeps surfacing, since it is a promise to someone else.

get_commitments.

See also

cdno questions

List active questions grouped by domain (research, life) — the orientation surface against the question system. For lifecycle changes (park / answer / retire / activate), use cdno question.

cdno questions [OPTIONS]

Options

Only the global options. With --json, emits the list as structured data.

Examples

cdno questions
cdno questions --json | jq '.[].slug'

get_active_questions — which additionally accepts a domain filter.

See also

cdno search

Full-text search across all notes, ranked best-first, with optional filters by note type, date window, and portfolio.

Titles are searched too, and weighted ten times a body match. A note whose title contains your words ranks above one that merely mentions them, so you do not need a separate command to look one up by name. When you already know which note you want and can name its slug or date, cdno open resolves it directly instead of ranking.

cdno search [OPTIONS] <QUERY>

Arguments

ArgumentDescription
<QUERY>Search text. Matched case-insensitively; terms are ANDed. Quotes and operators are treated as literal words.

Options

FlagDescription
--type <TYPE>Restrict to one note type (e.g. daily, project, evidence, or a config-defined custom type). A name that is neither built-in nor a registered custom type errors with the valid set; tab-completion offers your vault’s types.
--from <FROM>Inclusive earliest note date (YYYY-MM-DD).
--to <TO>Inclusive latest note date (YYYY-MM-DD).
--portfolio <PORTFOLIO>Restrict to notes in this portfolio.
--limit <LIMIT>Maximum results. Default 20.

Plus the global options. With --json, emits an array of hits, each with path, note_type, title, snippet, and score.

Examples

cdno search "preconditioner"
cdno search "sparse attention" --type evidence
cdno search "mesh" --from 2026-03-01 --to 2026-03-31 --limit 5
cdno search "speedup" --portfolio sparse-vs-dense-attention-ood --json | jq -r '.[].path'

search_notes.

See also

cdno review

Guided review rituals.

cdno review [OPTIONS] <COMMAND>

Subcommands

SubcommandDescription
weeklyWalk the retrospective sections into this week’s note and set next week’s goal.
monthlyWalk the monthly sections into this month’s note.

review ignores --json.


cdno review weekly

Walk the retrospective sections (Wins, Challenges, One Improvement) into this week’s note, then set next week’s goal as the This Week’s Goal of next week’s note. When run non-interactively, it reads the current note instead of prompting.

cdno review weekly [OPTIONS]

Options

Only the global options.

Examples

cdno review weekly                  # interactive: prompts for each section, sets next week's goal
cdno review weekly --no-interactive # read the current weekly note without prompting

cdno review monthly

Walk the monthly sections (Wins, Themes, Next Month’s Focus) into this month’s note. Unlike the weekly ritual there is no cross-note carry-forward — every section lands in the same month’s note. When run non-interactively, it reads the current note instead of prompting.

cdno review monthly [OPTIONS]

Options

Only the global options.

Examples

cdno review monthly                  # interactive: prompts for each section
cdno review monthly --no-interactive # read the current monthly note without prompting

get_weekly_context, read_weekly_note, upsert_weekly_section, read_monthly_note, upsert_monthly_section.

See also

cdno project

Manage project maps: create, update state, set the core question, hand off and take back, add/complete/drop milestones, manage waiting-on items, park/activate, complete/drop the project itself, and list/show. Next actions have their own verb, cdno action.

cdno project [OPTIONS] <COMMAND>

Subcommands

SubcommandDescription
createCreate a new project map
stateUpdate the Current State (auto-logs the previous)
core-questionSet or clear the core question (auto-logs the previous)
hand-offName who else is moving a project (auto-logs the change)
take-backMake a handed-off project yours again (auto-logs the change)
parkMove a project to _parked/
completeClose a project whose work is done (moves it to _done/<year>/)
dropClose a project that is not going to happen (moves it to _done/<year>/)
activateBring a parked or closed project back (enforces the cap)
listList active projects, or closed ones with --closed
showShow one project
milestoneAdd / complete / drop milestones
waitingAdd / resolve waiting-on items

Write subcommands honour --json (a {path, message} result, run non-interactively); list/show emit their data under --json.

A parked project is a complete map, so every subcommand that writes a project’s body (state, core-question, hand-off, take-back, milestone, waiting, and the cdno action verbs that take --project, except start, switch and resume) accepts an active or a parked slug. When --slug is omitted in a terminal, the picker and shell completion offer the active and parked projects; park offers active projects only. See Business rules.


cdno project create

Create a new project map. Created parked if you’re already at the active cap.

FlagDescription
--title <TITLE>Project title (the slug derives from it).
--slug <SLUG>Name the new map’s file yourself instead of deriving it from the title. Not a selector: the other subcommands use --slug to pick an existing project. Refused if invalid or taken; see the slug rule.
--context <CONTEXT>Life domain: work, side-project, university, family, household, legal, personal.
--question <QUESTION>Vault-relative core-question wikilink target (e.g. questions/research/foo). Optional.
--var <NAME=VALUE>Value for a custom template’s prompted variable ([variables.prompt]). Repeatable. See Prompted variables.
cdno project create --title "Surrogate model" --context work
cdno project create --title "Thesis" --context university --var ticket=ABC-123
cdno project create --title "Surrogate model for the mesh-scaling study" --context work --slug surrogate-model
cdno project create --title "Thesis" --context university --question questions/research/surrogate-cost

cdno project state

Update the Current State section. The previous state is auto-logged to today’s daily note first (see Business rules).

FlagDescription
--slug <SLUG>Active or parked project slug.
--text <TEXT>The new state text.
cdno project state --slug surrogate-model --text "Mesh scaling works; assembly is the bottleneck"

cdno project core-question

Set or clear the project’s core question after creation, auto-logging the previous value to today’s daily note in the same was: / now: shape state uses.

core-question — --slug, and either --question <target> or --clear

cdno project core-question --slug surrogate-model --question questions/research/does-it-scale
cdno project core-question --slug surrogate-model --clear

--question takes the bare wikilink target, the same form create --question takes — questions/research/foo, not [[…]], which is rejected rather than double-wrapped.

Passing neither flag non-interactively is an error, not a silent detach: dropping a project’s question is a decision and has to be asked for.

cdno project hand-off

Record that someone else is moving an open (active or parked) project: the map gets held_by: "<name>", and today’s daily note gets a holder on [[<slug>]] entry with indented was: / now: lines, the side with no holder written (mine). A hand-off records a fact about the project: someone else is moving it now, and the next move is theirs. Because the active cap counts the projects that are yours to move, an active project that names a holder is not counted; but handing off is never the answer to a full cap. Name a holder only when someone else really has the project; to make room, park, complete or drop one. The map stays where it is, and its deadlines stay in cdno commitments.

FlagDescription
--slug <SLUG>Active or parked project slug.
--to <WHO>Who has it: a name, or a wikilink target such as [[people/ana-ruiz]]. Prompted as “Who has it?”.
cdno project hand-off --slug hull-survey --to "Ana Ruiz"
# Handed hull-survey to Ana Ruiz (projects/hull-survey.md)

A hand-off is refused when:

  • the value names you or nobody (me, tbd, someone, or another entry of holder_sentinels): if you cannot name who has the project, it is yours. Keep it, or run cdno project take-back if it was handed off;
  • the value is a date, which names no one: give the person’s name instead;
  • an action on the project is in focus: pause it first with cdno action pause, then hand it off;
  • the project is closed: only an active or parked project has a holder, so activate it first if the work has come back.

Handing a project to the holder it already names changes nothing and logs nothing.

cdno project take-back

Clear a project’s holder: the map gets held_by: null and the daily note a holder on [[<slug>]] entry with now: (mine). An active project then counts toward the cap again, so taking one back at the cap is refused with the list of counted projects; park, complete or drop one of them first. A parked project stays parked: taking it back clears the name only and needs no slot.

FlagDescription
--slug <SLUG>Active or parked project slug.
cdno project take-back --slug hull-survey
# Took hull-survey back (projects/hull-survey.md)

cdno project park

Move an active project to projects/_parked/, freeing a slot against the five-project cap.

FlagDescription
--slug <SLUG>Active project slug.
cdno project park --slug surrogate-model

cdno project complete

Close a project because its work is done. The map moves to projects/_done/<year>/<slug>.md with status: completed and closed: set to today, and the daily log gets project completed [[<slug>]] — <title>. It works on an active or a parked project and never needs a slot, since closing adds nothing to the active count.

FlagDescription
--slug <SLUG>Active or parked project slug.

A completion is a claim that the work was done, so it is refused while any action or milestone is still open, and there is no flag to force it. The refusal lists what is open:

surrogate-model has 3 open items:
  - [ ] Run feature set B on full geometry mesh (deep)
  - [ ] [[actions/characterise-kan-ppo-sample-efficiency]] (deep)
  - [ ] ICML paper submitted — hard: 2026-05-22
Still open, not touched: [[commitments/reviewer-report]] (due 2026-10-15)
Complete or drop each of them (or add it to the project that now owns it), then run again.

Tick what was done (cdno action done, milestone done), drop what is not happening (cdno action drop, milestone drop, each with its own reason), and run the command again:

cdno project complete --slug surrogate-model
# Completed surrogate-model. 4 of 5 slots in use.

Standalone commitments linked to the project are never touched: a promise to someone else does not end with the project. The success message names any that are still open.

cdno project drop

Close a project that is not going to happen. The map moves to projects/_done/<year>/<slug>.md with status: dropped and closed: set to today, and the daily log gets project dropped on [[<slug>]] — <title>, with the reason on an indented reason: line. Like complete, it works on an active or a parked project and never needs a slot: ending a shelved plan does not bring it back first.

FlagDescription
--slug <SLUG>Active or parked project slug.
--reason <REASON>Why it is being dropped. Optional and never prompted for, but worth giving.
--drop-openDrop the project’s open actions and milestones with it, without asking.

With open actions or milestones, drop lists them as complete does. In a terminal it then asks Let these N go and drop <slug>? [y/N], and pressing Enter keeps everything. On y each open item is dropped with the project, logged as its own action dropped on / milestone dropped on line with reason: project dropped (<your reason>) (or plain reason: project dropped without --reason), and an attached action note is archived as dropped. If the list changed while you were answering, nothing is dropped and the new list is shown. Non-interactively the list is printed and the command exits 1, unless you pass --drop-open.

cdno project drop --slug bayesian-opt-survey --reason "superseded by the ICML work"
cdno project drop --slug bayesian-opt-survey --reason "superseded" --drop-open
# Dropped bayesian-opt-survey. 4 of 5 slots in use. Let go: 2 actions, 1 milestone.

Items that were in fact done should be ticked first: a drop records that work was let go, not that it was finished.

Under --json a refusal from either verb prints the same project_has_open_items object the MCP server returns (the items plus an open_items_hash) and exits 1; a success prints path, message, dropped_actions, dropped_milestones and untouched_commitments.

A closed project can be closed again with the other outcome, as a new decision: a dropped project that turns out to have been finished can be completed, and the reverse. It is re-filed under this year’s folder with today’s closed: date and a new log line. Closing it again with the same outcome is refused, naming the date it already has.

cdno project activate

Bring a parked, completed or dropped project back to projects/. A closed project also has its closed: date cleared, so it can be closed again later with a fresh one. The log says project [[<slug>]] activated. Fails if it would exceed the active cap — park another first. The interactive picker and shell completion offer parked and closed projects, labelled.

FlagDescription
--slug <SLUG>Parked or closed project slug.
cdno project activate --slug surrogate-model

cdno project list

List active projects with a state snippet. Each project renders as a card — a coloured bar keyed to its context, the slug as a title, and the state wrapped underneath. Honours --json.

A project that names someone else as its holder carries one more line, held by: <name> · does not count toward the cap. A value on holder_sentinels leaves the map yours, so it prints no holder line. Under --json each row carries held_by and counts (see JSON output).

In a terminal this then offers to open one of the projects it just listed, printing the same thing cdno project show would and asking again until you press Esc. Piped output, --no-interactive, and --json skip the prompt. See Colour and interactivity.

With --closed it lists completed and dropped projects instead, newest first, each with its outcome and closing date. Under --json each row carries slug, title, context, outcome and closed_on.

cdno project list
cdno project list --json | jq '.[].slug'
cdno project list --no-interactive     # listing only, never a prompt
cdno project list --closed

cdno project show

Show a compact summary of a single project (any status). The slug is an optional positional: omit it in a terminal and cdno offers a picker covering active and parked projects. Honours --json (emits the project summary object).

For an active or parked project it ends with Held by: <name> when the map names someone else (a sentinel prints none), and Counts toward the cap: yes or no.

cdno project show surrogate-model
cdno project show                      # pick from a list
cdno project show surrogate-model --json

cdno project milestone

Manage milestones — markers of progress. A --hard milestone is a real deadline counted in cdno commitments.

add — --slug, --title, --date <YYYY-MM-DD> (optional), --hard done — --slug, --query (case-insensitive substring of the milestone title)

drop — --slug, --query, --reason (optional)

cdno project milestone add --slug surrogate-model --title "Submit to ICML" --date 2026-01-22 --hard
cdno project milestone add --slug surrogate-model --title "All Round-1 replies received"
cdno project milestone done --slug surrogate-model --query "submit to icml"
cdno project milestone drop --slug surrogate-model --query "book the venue" --reason "the funder withdrew"

Use drop rather than done when the milestone is not going to happen — superseded, mis-typed, or overtaken by events. done ticks the bullet and writes milestone done on ... to the daily log, asserting a milestone that was met; drop removes the bullet and writes milestone dropped on ... instead, so a later reader can tell a plan that changed from a plan that was kept. --reason is optional and never prompted for: a correction is simply a drop with no reason. Only open - [ ] bullets are matched — a completed milestone is a record of what happened, not a plan to revise.

--date is optional. Some milestones are gated by a condition rather than a date — “all Round-1 replies received” on a correspondence-driven project — and omitting the flag records the milestone as target: TBD rather than making you invent an estimate. An undated milestone does not appear in cdno commitments, which is the point: a date you made up reads later like a commitment somebody made. It completes with done exactly like a dated one.

Interactively, the calendar is offered behind a yes/no so the undated case is reachable without knowing the flag can be omitted.

--hard requires --date: a hard deadline with no date is rejected rather than quietly downgraded to a soft target.

cdno project waiting

Track external blockers.

add — --slug, --description resolve — --slug, --query (substring of the item)

cdno project waiting add --slug surrogate-model --description "Cluster quota from IT"
cdno project waiting resolve --slug surrogate-model --query "cluster quota"

create_project, update_project_state, park_project, complete_project, drop_project, activate_project, hand_off_project, list_projects, get_project_context, add_milestone, complete_milestone, set_core_question, add_waiting_on, resolve_waiting_on.

See also

cdno action

Manage a project’s next actions: add (optionally as a manifest note), promote a bullet to a note, start, pause, switch, resume, complete, drop, and list.

cdno action [OPTIONS] <COMMAND>

Subcommands

SubcommandDescription
addAppend a next action to a project
startLog that work on an action is starting
pauseSet the action in focus aside without finishing it
switchPause the action in focus and start another, in one step
resumePick a carried or paused action up again
promotePromote a plain bullet to a wikilinked manifest note
completeMark an action done by substring match
dropClose an action without recording it as done
listList a project’s open actions

Write subcommands honour --json ({path, message}, non-interactive); list emits its data under --json. A refused start, pause, resume or switch prints a rejection object instead; see When start is refused.

start, pause, switch and resume are the focus verbs: they move the one slot that cdno now reads. See Focus for the idea and the log markers they write below.


cdno action add

Append a next action to a project. --note also scaffolds a manifest note and wikilinks the bullet.

FlagDescription
--project <SLUG>Active or parked project slug.
--title <TITLE>Action title.
--energy <ENERGY>deep, medium, or light.
--noteAlso create a manifest note alongside the bullet and wikilink it.
--slug <SLUG>Name the action note’s file yourself instead of deriving it from the title. Only with --note: a plain bullet has no file. Refused if invalid or taken; see the slug rule.
--var <NAME=VALUE>Value for a custom action-note template’s prompted variable ([variables.prompt]). Repeatable. Only applies with --note (a plain bullet isn’t templated). See Prompted variables.
cdno action add --project surrogate-model --title "Profile the assembly step" --energy medium
cdno action add --project surrogate-model --title "Characterise sample efficiency" --energy deep --note
cdno action add --project surrogate-model --title "Rerun the ablation on the held-out split with feature set B" --energy deep --note --slug feature-set-b-ablation

cdno action promote

Promote an existing plain bullet to a wikilinked manifest note. Substring-matches the bullet text; energy is inherited.

FlagDescription
--project <SLUG>Active or parked project slug.
--query <QUERY>Case-insensitive substring of the bullet text.
--slug <SLUG>Name the new action note’s file yourself instead of deriving it from the bullet. Refused if invalid or taken; see the slug rule.
--var <NAME=VALUE>Value for a custom action-note template’s prompted variable ([variables.prompt]). Repeatable. Promotion scaffolds an action note, so it gathers the same prompts as add --note. See Prompted variables.
cdno action promote --project surrogate-model --query "profile the assembly"
cdno action promote --project surrogate-model --query "profile the assembly" --slug profile-assembly

cdno action start

Log that work on an action is starting: writes started [[slug]] — <bullet> to today’s daily note, which is what cdno now reads back.

The action must already be on the map. A start names a bullet, so that the later completion logs matching text and the focus clears — a start naming nothing could never be closed. What gets logged is the resolved bullet text, not your query, so --query "draft methods" logs Draft methods (deep).

cdno action promote rewrites the bullet it matched, and the focus follows it: cdno now switches to the new note ([[actions/<slug>]] (deep)), keeps the original start time, and complete and drop still pair with the start.

FlagDescription
--project <SLUG>Active project slug: starting work needs an active map.
--query <QUERY>Substring of an existing bullet. Conflicts with --unplanned.
--unplannedStart work that is on no map yet: adds the bullet, then starts it.
--title <TEXT>Title for the new bullet. Requires --unplanned.
--energy <LEVEL>deep, medium or light. Requires --unplanned.

When start is refused

Focus is one slot, so start while another action is in focus is refused, naming the open one and the way forward:

$ cdno action start --project other-project --query "CI"
Error: Draft the methods section is already in focus on surrogate-model (since 09:30).
To move on to this instead: cdno action switch --project other-project --query CI
Or complete or pause it first.

You can always move on; you just say so. In a terminal the refusal is followed by Switch to it instead? (default no); y runs the switch without asking for a hint or a reason. Piped, with --no-interactive or with --json, it fails fast instead.

Starting the action that is the focus is refused too. If it was started today there is nothing to do. If it was carried over from an earlier day the message points at cdno action resume. A typo or an ambiguous --query is reported as such, not as a refusal, and a refused --unplanned start adds no bullet.

With --json the refusal is the same object the MCP server returns, on stdout, with a non-zero exit:

{
  "code": "focus_open",
  "message": "An action is already in focus. Ask the person before switching; do not retry.",
  "details": {
    "focus": {"project": "surrogate-model", "action": "Draft the methods section (deep)",
              "started": "09:30", "date": "2026-10-04", "carried": false},
    "attempted": {"project": "other-project", "query": "CI"},
    "same_action": false,
    "remedy": "switch_action"
  }
}

remedy is switch_action when something else is in focus, already_focused for the same action started today, and resume_action for the same action carried over. attempted is {project, query}, or {project, title} for an --unplanned start.

An ambiguous --query is a question rather than a dead end: in a terminal you get a picker over the candidates, and non-interactively they are listed one per line. The same holds for complete, drop and promote, which resolve through the same matcher.

One case no picker can settle: two open bullets whose text differs only in case, or not at all. The domain’s whole-bullet tiebreak compares case-insensitively, so it sees two exact matches, declines, and picking either re-ambiguates. Edit one of the bullets to tell them apart.

# start something already planned
cdno action start --project surrogate-model --query "feature set B"

# start something that was never planned — adds the bullet and starts it
cdno action start --project surrogate-model --unplanned \
    --title "Fix the CI badge" --energy light

--unplanned is deliberately explicit rather than a fallback when --query matches nothing: a fallback would turn every typo into a new action, silently. --title and --energy require it, so passing them alone is a parse error naming --unplanned rather than a start on some other bullet.

cdno action pause

Set the action in focus aside without finishing it. It acts on the current focus and takes no project or query: there is exactly one focus, and it is the one paused. Nothing is looked up on the project map, so a focus on a project you have since parked can still be paused, and the bullet stays where it is.

FlagDescription
--next <TEXT>Where to pick up again: the re-entry hint resume and cdno now read back. In a terminal, asked for once when absent (Enter skips); no confirmation follows.
--reason <TEXT>Why the work was set aside. Never prompted for.
cdno action pause --next 'pick up at "Prior approaches"' --reason "CI is red"

With nothing in focus it says Nothing started — nothing to pause. and exits non-zero. A pause is not a drop: a paused action stays open on the map, and can be completed or dropped later as usual.

cdno action switch

Pause the action in focus and start another, in one commit. It takes the same target flags as start, plus the hint and reason for the action being set aside.

FlagDescription
--project <SLUG>Active project slug of the action to switch to: switching work needs an active map.
--query <QUERY>Substring of the open bullet to switch to. Conflicts with --unplanned.
--unplannedSwitch to work on no map yet: adds the bullet, then starts it.
--title <TEXT>Title for the new bullet. Requires --unplanned.
--energy <LEVEL>deep, medium or light. Requires --unplanned.
--next <TEXT>Re-entry hint for the action being paused. Asked for once in a terminal when absent (Enter skips).
--reason <TEXT>Why the focus moved. Never prompted for.
cdno action switch --project other-project --query "CI" \
    --next "related-work paragraph half done" --reason "a collaborator is blocked on it"

A switch is a pause followed by a start in the log; there is no separate marker. If the target does not resolve, nothing is written, not even the pause. With nothing in focus it is a plain start, and a --next it had nothing to attach to is reported rather than dropped. A prompted switch shows pause <X>, start <Y> and asks to confirm, and the picker leaves out the bullet that is already in focus.

cdno action resume

Pick work up again. It takes no query or title: the text comes from the log, not from the map.

FlagDescription
--project <SLUG>Restrict to this project: its carried focus, else its most recent pause.
$ cdno action resume
Resumed on surrogate-model, logged to journal/2026/daily/2026-10-04.md
next: pick up at "Prior approaches"

Without --project it resumes the focus carried over from an earlier day if there is one, else the most recent pause within paused_lookback_days (default 14). It writes resumed [[slug]] — <text>, which re-anchors the focus to now, so a Tuesday start you keep working on does not expire on Thursday, and prints the pause’s next: hint when it has one. A pause that a later start, resume, completion, drop or promotion of the same action followed is no longer offered.

With nothing to pick up it says Nothing to resume — nothing is carried over or paused. (Nothing to resume on <project> — … with --project) and exits non-zero. It is refused with a focus_open refusal (the same object as above, without attempted) when a different action is in focus (pause it or switch instead; resume never switches for you) or when the same action was already started today. With --json a success carries path, message and resumed_from: kind (carried or paused), date, next and reason.

What the focus verbs write

Each verb is one entry in today’s ## Logs. next: and reason: are indented continuation lines, written only when given:

- **10:40**: action paused on [[surrogate-model]] — Draft the methods section (deep)
  next: pick up at "Prior approaches"
  reason: a collaborator is blocked on CI
- **10:40**: started [[other-project]] — Fix red CI on main (medium)
- **14:05**: resumed [[surrogate-model]] — Draft the methods section (deep)

paused closes the focus the way action done on and action dropped on do; resumed opens it again. complete and drop are unchanged, including on a paused action. When a focus is open, a log line written by cdno log, cdno capture or the triage verbs carries a during: tag naming its project.

cdno action complete

Mark a next action completed by case-insensitive substring match. A wikilinked bullet also archives its note to actions/_done/<year>/.

FlagDescription
--project <SLUG>Active or parked project slug.
--query <QUERY>Substring of the bullet text.
cdno action complete --project surrogate-model --query "feature set B"

cdno action drop

Close a next action without recording it as done — for work that was superseded, abandoned or reprioritised. Matches the bullet exactly as complete does; a wikilinked bullet also archives its note to actions/_done/<year>/, stamped status: dropped with no completion date.

FlagDescription
--project <SLUG>Active or parked project slug.
--query <QUERY>Substring of the bullet text.
--reason <TEXT>Optional: why it was dropped.
cdno action drop --project surrogate-model --query "demo proposal" \
    --reason "superseded by the demo-planning action"

Use this rather than complete whenever the work was not actually performed. complete writes action done on [[…]] into the daily log, which is the record the weekly review, the monthly scan and every later verdict read back from — so completing something that never happened leaves the vault asserting work nobody did, and the only repair is a correction line written by hand.

--reason is optional but worth giving: “superseded by X” and “no longer wanted” are different facts, and only one of them tells a later reader to go looking for the replacement. It is recorded on a continuation line under the log entry.

A dropped action does not appear in the completed-actions views that the weekly and monthly reviews build, because it carries no completion date.

cdno action list

List a project’s open action bullets, with attached-note status (active / blocked / completed / dropped) inline when present. Honours --json.

FlagDescription
--project <SLUG>Active or parked project slug.
cdno action list --project surrogate-model
cdno action list --project surrogate-model --json

add_action, promote_action, start_action, start_unplanned_action, pause_action, switch_action, switch_unplanned_action, resume_action, complete_action, drop_action. (Open actions are also visible via get_project_context; what is currently started via current_focus.)

See also

cdno portfolio

Manage evidence portfolios: create, list, show, and link to a question or project. Filing evidence into a portfolio is the separate cdno file verb.

cdno portfolio [OPTIONS] <COMMAND>

Subcommands

SubcommandDescription
createCreate a portfolio under portfolios/<slug>/
listList portfolios with evidence counts + staleness
showShow a portfolio’s frontmatter + evidence inventory
linkLink an existing portfolio to a question or project

create/link honour --json ({path, message}); list/show emit their data under --json.


cdno portfolio create

Create a new portfolio. The slug derives from the question.

--question takes either free text or the slug of an existing question. Given a question’s slug, the portfolio takes that question’s text as its title, the two are linked both ways, and the portfolio’s slug is the question’s. An explicit --slug must then be omitted or equal to that slug; any other value is refused, because the pair is correlated by slug.

FlagDescription
--question <QUESTION>The question this dossier accumulates evidence for: its text, or the slug of an existing question.
--slug <SLUG>Name the portfolio’s folder yourself instead of deriving it from the question. Must match the question’s slug when --question names one. Refused if invalid or taken; see the slug rule.
--project <PROJECT>Optional wikilink to a project to associate it with.
--var <NAME=VALUE>Value for a custom template’s prompted variable ([variables.prompt]). Repeatable. See Prompted variables.
cdno portfolio create --question "Sparse vs dense attention OOD"
cdno portfolio create --question "Sparse vs dense attention OOD" --project projects/surrogate-model
cdno portfolio create --question "Sparse vs dense attention OOD" --slug sparse-vs-dense
cdno portfolio create --question surrogate-generalises          # from an existing question's slug

cdno portfolio list

List every portfolio with its evidence count and staleness. Honours --json.

In a terminal this then offers to open one of the portfolios it just listed, printing what cdno portfolio show would and asking again until you press Esc. Piped output, --no-interactive, and --json skip the prompt. See Colour and interactivity.

cdno portfolio list
cdno portfolio list --json | jq '.[] | {slug, evidence_count}'

cdno portfolio show

Show a portfolio’s frontmatter and its evidence inventory. Honours --json (a detail object with the evidence list).

FlagDescription
--portfolio <PORTFOLIO>Portfolio slug.
cdno portfolio show --portfolio sparse-vs-dense-attention-ood
cdno portfolio show --portfolio sparse-vs-dense-attention-ood --json

Link an existing portfolio to an existing question or project (the retrofit path — pass exactly one of --question/--project). Backlinks are set on both sides.

FlagDescription
--portfolio <PORTFOLIO>Portfolio slug.
--question <QUESTION>Question to link (mutually exclusive with --project).
--project <PROJECT>Project wikilink to link (mutually exclusive with --question).
cdno portfolio link --portfolio sparse-vs-dense-attention-ood --project projects/surrogate-model
cdno portfolio link --portfolio sparse-vs-dense-attention-ood --question questions/research/surrogate-cost

create_portfolio, get_portfolio_contents (show), link_portfolio_to_question, link_portfolio_to_project.

See also

cdno file

File a piece of evidence into a portfolio. Without --attach it writes a plain Markdown evidence note; with --attach it copies a non-Markdown artefact (PDF, image, video) into the portfolio and scaffolds a linked evidence stub beside it.

cdno file [OPTIONS]

Options

FlagDescription
--portfolio <PORTFOLIO>Portfolio slug. (Prompted/fuzzy-picked if omitted interactively.)
--source <SOURCE>Citation, experiment id, conversation reference, …
--origin <ORIGIN>Bare wikilink target to whatever produced this evidence (e.g. projects/foo); the CLI wraps it into [[...]].
--content <CONTENT>Inline body. For a plain note it’s the content; with --attach it’s the abstract. Optional; defaults to empty.
--attach <ATTACH>Path to a non-Markdown artefact. Copied into portfolios/<slug>/<evidence-slug>/ with a stub that links to it.
--moveWith --attach, remove the source file after a successful copy (move instead of copy).
--var <NAME=VALUE>Value for a custom evidence template’s prompted variable ([variables.prompt]). Repeatable. Ignored with --attach (attachment stubs aren’t templated). See Prompted variables.

Plus the global options. With --json, emits a {path, message} result and runs non-interactively.

Examples

# A plain prose evidence note:
cdno file --portfolio sparse-vs-dense-attention-ood \
          --source "Chen et al. 2025, NeurIPS" \
          --origin projects/surrogate-model \
          --content "4x speedup at 95% accuracy on the OOD split."

# Attach a PDF (copied into the portfolio; --content is the abstract):
cdno file --portfolio sparse-vs-dense-attention-ood \
          --source "Chen et al. 2025" --origin projects/surrogate-model \
          --attach ~/Downloads/chen2025.pdf --content "Key result: 4x speedup."

# Move the artefact in instead of copying:
cdno file --portfolio sparse-vs-dense-attention-ood --source "fig 3" \
          --origin projects/surrogate-model --attach ./fig3.png --move

Evidence notes are append-only.

file_to_portfolio.

See also

cdno question

Manage question notes: create one, then transition its status. Each status transition is logged to today’s daily note. To list active questions, use cdno questions.

cdno question [OPTIONS] <COMMAND>

Subcommands

SubcommandDescription
createCreate a new question note
parkSet status to parked
answerSet status to answered
retireSet status to retired
activateSet status to active

All honour --json ({path, message}, non-interactive).


cdno question create

Create a new question under questions/<domain>/<slug>.md. The slug derives from the text.

FlagDescription
--domain <DOMAIN>research or life.
--text <TEXT>The question text (becomes the body H1).
--slug <SLUG>Name the file yourself instead of deriving it from the text. Refused if invalid or taken; see the slug rule.
--var <NAME=VALUE>Value for a custom template’s prompted variable ([variables.prompt]). Repeatable. See Prompted variables.
cdno question create --domain research --text "Does sparse attention beat dense OOD?"
cdno question create --domain research --text "Does the surrogate model generalise to unseen meshes?" --slug surrogate-generalises

Status transitions

park, answer, retire, and activate each take a --slug. The interactive picker offers only eligible questions for that transition (e.g. you can only park an active question).

FlagDescription
--slug <SLUG>Question slug.
cdno question park   --slug does-sparse-attention-beat-dense-ood
cdno question answer --slug does-sparse-attention-beat-dense-ood
cdno question retire --slug does-sparse-attention-beat-dense-ood
cdno question activate --slug does-sparse-attention-beat-dense-ood

create_question, set_question_status. (List via get_active_questions.)

See also

cdno stewardship

Manage stewardship dashboards: create (flat or expanded), list, show, and append a periodic commitment line. Filing a tracking entry is the separate cdno track verb.

cdno stewardship [OPTIONS] <COMMAND>

Subcommands

SubcommandDescription
createCreate a stewardship (flat, or expanded with --tracking)
listList stewardships with variant, tracking count, staleness
showShow a stewardship’s frontmatter + dashboard excerpt
add-periodicAppend a periodic commitment line
complete-periodicComplete one occurrence, rolling next: forward

create/add-periodic/complete-periodic honour --json ({path, message}); list/show emit their data under --json.


cdno stewardship create

Create a stewardship dashboard. --tracking makes it expanded (a stewardships/<slug>/ folder with room for tracking/ and routines/); without it, the dashboard is a single flat file.

FlagDescription
--name <NAME>Human-readable name (the slug derives from it).
--slug <SLUG>Name the dashboard’s file (flat) or folder (expanded) yourself instead of deriving it from the name. Refused if invalid or taken; see the slug rule.
--context <CONTEXT>Life domain (work, household, personal, …).
--trackingCreate the expanded variant with a tracking/ folder.
--var <NAME=VALUE>Value for a custom template’s prompted variable ([variables.prompt]). Repeatable. See Prompted variables.
cdno stewardship create --name "Finances" --context household           # flat
cdno stewardship create --name "Health" --context personal --tracking   # expanded
cdno stewardship create --name "Household finances and taxes" --context household --slug finances

cdno stewardship list

List every stewardship with its variant, tracking count, and staleness badge. Honours --json.

In a terminal this then offers to open one of the stewardships it just listed, printing what cdno stewardship show would and asking again until you press Esc. Piped output, --no-interactive, and --json skip the prompt. See Colour and interactivity.

cdno stewardship list
cdno stewardship list --json | jq '.[] | {slug, variant}'

cdno stewardship show

Show a stewardship’s frontmatter and an excerpt of the dashboard body. Honours --json (a detail object including variant and body_markdown).

FlagDescription
--slug <SLUG>Stewardship slug.
cdno stewardship show --slug health

cdno stewardship add-periodic

Append a periodic commitment line to the dashboard’s ## Periodic Commitments section. The line becomes a row in the aggregated cdno commitments view.

FlagDescription
--stewardship <SLUG>Stewardship slug.
--title <TITLE>Commitment title (e.g. “Dental check-up”).
--every <RECURRENCE>Recurrence: daily, weekly, monthly, yearly, or every N months. See Recurrence syntax.
--next <YYYY-MM-DD>Next due date.
cdno stewardship add-periodic --stewardship health --title "Dental check-up" \
     --every "every 6 months" --next 2026-09-01

cdno stewardship complete-periodic

Complete one occurrence of a periodic commitment, rolling its next: date forward by that line’s own recurrence. Named to pair with add-periodic: it completes an occurrence, not the stewardship, which is perpetual and never completes.

FlagDescription
--stewardship <SLUG>Stewardship slug.
--title <SUBSTRING>Case-insensitive substring of the commitment’s title.
--at <YYYY-MM-DD>Date the work was actually done. Defaults to today.
cdno stewardship complete-periodic --stewardship health --title "dental"
cdno stewardship complete-periodic --stewardship health --title "dental" --at 2026-08-25

Before this verb there was no way to mark a periodic commitment done: it is a bullet, not a note, so cdno commit done has nothing to act on, and the reminder kept firing until the file was hand-edited — the one edit the vault asks you not to make.

The next date is computed from the due date, never from --at. A check-up every 6 months, done a week early each time, would creep a week earlier every cycle if the schedule followed the work. Anchored to the due date, a run of early completions leaves the schedule where it was. A late completion advances until next: is in the future, so one neglected commitment comes back on schedule rather than several reminders deep.

One case the calendar imposes: a monthly commitment on the 31st has no 31st to land on in February, so it clamps to the 28th and keeps the 28th thereafter — the bullet records the next date, not the day the schedule nominally wants.

The entry records the completion and where the schedule moved to:

- **09:30**: periodic done on [[health]] — Dental check-up
  was: 2026-09-01
  now: 2027-03-01

A line whose recurrence the parser cannot read is refused rather than guessed — see Recurrence syntax for the accepted forms. Such a line is otherwise unaffected: it still appears in cdno commitments and lint still accepts it.

create_stewardship, get_stewardship_tracking, add_periodic_commitment, complete_periodic.

See also

cdno track

File a tracking note under an expanded stewardship. The activity is positional and selects the template: a vault’s .cuaderno/templates/tracking-<activity>.md if present, else the generic built-in. No activity-specific templates ship built-in — ready-made gym/body/swim variants are in examples/templates/tracking/.

cdno track [OPTIONS] <ACTIVITY>

Arguments

ArgumentDescription
<ACTIVITY>Activity slug — anything you track (e.g. gym, swim, reading). Selects tracking-<activity>.md if present, else the generic template.

Options

FlagDescription
--stewardship <STEWARDSHIP>Stewardship slug. Defaults to the only expanded stewardship if there’s exactly one; otherwise required.
--routine <ROUTINE>Bare slug of a routine doc — wrapped into [[stewardships/<slug>/routines/<routine>]] and substituted into the template’s routine: field. Only takes effect when the resolved template has a routine: field; the generic default has none, so it silently no-ops there.
--content <CONTENT>Inline body. Optional; defaults to empty so you can fill in tables afterward.
--at <WHEN>File the entry at a past (or near-future) day rather than today: YYYY-MM-DD, or a full YYYY-MM-DDTHH:MM[:SS] as cdno log --at takes (only the date is kept). For a session recorded after the fact — a statement reconciled days later, a reading taken this morning. Only the date is kept; the entry lands on that day and the daily-log line still goes into today’s note, naming the day it describes. Bounded to 50 years back and 1 year ahead: an unbounded date makes history writable, and a mistyped year would silently reshape a trend.
--var <NAME=VALUE>Value for a custom tracking template’s prompted variable ([variables.prompt]). Repeatable. Prompts come from the activity’s template (e.g. tracking-gym) when one exists. See Prompted variables.

Plus the global options. With --json, emits a {path, message} result and runs non-interactively.

Examples

cdno track gym --stewardship health --content "Upper body A; RDL up to 25kg"
cdno track body --stewardship health --content "Weight 78.4kg, resting HR 54"
cdno track swim --stewardship health --content "1500m, 28min"

# --routine needs a template with a routine: field (the example gym.md has one):
cdno track gym --routine upper-body-a

# With one expanded stewardship, --stewardship can be omitted:
cdno track gym --content "Legs day"

Tracking entries are append-only and only land in expanded stewardships (those created with --tracking).

Until a vault has any tracking template, cdno track prints a one-line hint (on stderr) pointing at examples/templates/tracking/ for a structured layout. It goes quiet once you author a template, and is suppressed under --json.

Merging a day

A second cdno track for the same activity and date merges into the first rather than erroring: --content is appended to the entry’s ## Notes, and any metrics are folded into its frontmatter. Recording a day in two passes — a morning and an evening session, spending logged as it happens — is ordinary rather than exceptional.

Merging is not blind concatenation. A record carrying a stable id replaces the record with that id, so re-applying the same payload is idempotent; a record without one appends, so re-running an import that omits ids double-counts every summed metric. Scalars are last-write-wins: there is no array to key on, and the later reading is what a level means — but sending a plain value for a key that already holds records is refused, since that would discard the day’s entries on a note type that only grows.

create_tracking_entry.

See also

cdno templates

Inspect and edit note templates. Use vars before writing a custom template in .cuaderno/templates/ to see which {{placeholders}} a note type supports — unknown placeholders render verbatim, so this is how you learn the valid set without reading the source.

CommandWhat it does
vars <TYPE>List the {{placeholders}} a type’s template supports.
listEvery note type, which template is in effect, and where its override lives.
show <TYPE>Print a template’s effective content verbatim.
eject <TYPE>Copy a built-in template into .cuaderno/templates/ to customise.
sync <TYPE>Add frontmatter keys a release added to a built-in template to your customised copy, keeping your edits.
saveWrite a template, from a file, from stdin, or via $EDITOR.
newScaffold a starter template for a config-defined custom type.

cdno templates vars <type>

List the {{placeholders}} a note type’s template supports.

cdno templates vars [OPTIONS] <TYPE>

Arguments

ArgumentDescription
<TYPE>Note type: project, action, question, portfolio, evidence, stewardship, tracking, commitment, daily, weekly, inbox, or a config-defined custom type.

Takes only the global options.

Sources

Each placeholder is classified by where its value comes from:

SourceMeaning
suppliedFilled automatically by the note type’s create command. This is the type’s complete create-path key set — including body placeholders and keys the default template happens not to reference (e.g. daily’s weekday, tracking’s routine) — so it matches the per-type table in Customising templates and frontmatter exactly.
configA static [variables] entry in .cuaderno/config.toml, available to any template.
promptA [variables.prompt] entry — a value must be provided at creation (via --var name=value, the MCP vars parameter, or interactively). The prompt message is shown.

A config or prompt name that collides with a supplied key is omitted: the supplied value shadows it, so it would never take effect.

With --json, emits an array of { name, source } objects (prompt entries also carry message).

Examples

cdno templates vars project
cdno templates vars tracking
cdno templates vars question --json | jq -r '.[].name'

cdno templates eject <type>

Copy a built-in template into .cuaderno/templates/<type>.md as an editable starting point. Note types use an in-binary default until you add a file for them (only daily is seeded on cdno init); this materialises one so you can customise it (add sections, reorder frontmatter, reference {{placeholders}} from templates vars) without hand-reconstructing it from the source tree.

cdno templates eject [OPTIONS] <TYPE>

Arguments

ArgumentDescription
<TYPE>Built-in note type to eject. Omit when using --all. A config-defined custom type has no built-in template to eject, so it is refused here — use templates new to scaffold one instead. Unlike templates vars, which does accept custom types.

Options

FlagDescription
--allEject every built-in template into .cuaderno/templates/ at once. Types that already have a template file are skipped (a summary reports which), unless --force. Mutually exclusive with <TYPE> — pass one or the other.
--forceOverwrite existing custom template(s). Without it, an existing file is left untouched (and single-type eject errors; --all skips it).

Plus the global options. With --json, single-type eject emits the { path, message } write result; --all emits an object with written and skipped arrays (note-type names).

Only base note-type templates eject — no <type>-<variant> template ships built-in. To create a tracking activity variant, copy one from examples/templates/tracking/ to .cuaderno/templates/tracking-<activity>.md instead.

Examples

cdno templates eject project              # → .cuaderno/templates/project.md
cdno templates eject tracking             # → the generic tracking template
cdno templates eject project --force      # overwrite an earlier customisation
cdno templates eject --all                # eject all built-ins, skip customised
cdno templates eject --all --force        # eject all, overwriting everything

The written file is exactly the built-in default, so a note created straight after ejecting is byte-identical to before — customise from there.

cdno templates sync <type>

Bring a customised template up to date with frontmatter keys a later release added to the built-in one, without losing your customisation. A file you ejected or saved never receives such keys by itself; nothing breaks without them (new notes from it just lack the key), but nothing else adds them either, and eject --force would throw your edits away.

cdno templates sync [OPTIONS] <TYPE>
cdno templates sync --all [--check]
Argument / flagDescription
<TYPE>Built-in note type whose override to sync. A config-defined custom type, including a bundled one such as concept, has no built-in to compare with and is refused: its template is kept up to date by hand.
--allSync every built-in type that has a custom override.
--checkReport what is missing and write nothing; exit non-zero if any override is behind. For CI and scripts.

For each key of the built-in template’s frontmatter that your file lacks, sync inserts the built-in line verbatim: a literal such as closed: null, or a placeholder line such as core_question: {{core_question}}. It goes right after the nearest built-in key that precedes it and your file has: after that key’s whole block, or, when that key has a one-line plain value, straight after its line, so a comment above your next key stays with it; failing that, right before the nearest built-in key that follows it; failing that, last in the frontmatter. A key your file already has, quoted ("closed": null) or with a space before the colon, counts as present. Nothing else changes: your own keys, their values, their order and the body stay byte for byte, and no key is ever removed. Running it again on an up-to-date file writes nothing.

cdno templates sync project
# project: added `closed` to .cuaderno/templates/project.md; kept your `owner`
cdno templates sync --all --check     # in CI: fails while any override is behind

--json emits an array with one object per override (with <TYPE> and no override, one not_customised object), each with note_type, path, status (synced, behind, up_to_date, not_customised, no_frontmatter or unreadable, the last with an error), added and kept (your own keys, listed so you can see nothing was dropped). A file with no --- frontmatter block, or one that cannot be read, is reported and left alone; an unreadable one makes the command exit non-zero, with or without --check. It writes through the same path as templates save.

cdno templates list and cdno lint both say when an override has fallen behind.

cdno templates list

Every note type with the state of its template: whether a custom override exists, which source is in effect, and the path the override lives (or would live) at. A custom override that lacks a key its built-in has gained shows custom (lacks closed; …) in the Template column.

cdno templates list [OPTIONS]

--json reports source as a stable token — builtin_default, builtin_variant, custom_base, custom_variant, or none — rather than the human label in the table, and lists the missing keys under missing_builtin_keys (empty when there are none).

cdno templates show <type>

Print a template’s effective content verbatim: the custom override when one exists, else the built-in default. A custom type with no template yet shows the starter new would write.

cdno templates show [OPTIONS] <TYPE>
OptionDescription
--variant <NAME>Show a <type>-<variant> template instead of the base one. Built-in types only.

Output is byte-verbatim, so it round-trips:

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

--json emits {content, source} instead.

cdno templates save

Write a template. On a built-in type this creates the custom override transparently — no prior eject needed.

cdno templates save [OPTIONS]
OptionDescription
--note-type <TYPE>The type whose template to write.
--variant <NAME>Write the <type>-<variant> template. Built-in types only.
--file <PATH>Read the new content from this file, or from stdin when it is -.

Without --file, an interactive run opens $EDITOR seeded with the template as it stands. Off a terminal --file is required, so a scripted save cannot silently truncate a template to nothing.

cdno templates save --note-type project --file my-project.md
cat my-project.md | cdno templates save --note-type project --file -

cdno templates new

Scaffold a starter template for a config-defined custom type that has none yet — a frontmatter block of type plus each declared required field as a {{placeholder}}, and a # {{title}} heading.

cdno templates new [OPTIONS]
OptionDescription
--note-type <TYPE>The custom type to scaffold for.

Refuses a built-in type, which already has a default to edit — use eject or save for those — and refuses to overwrite an existing template.

A custom type has exactly one template, so --variant is not accepted on show, save or new for one; declare a separate note type instead.

cdno config

Inspect, check and edit .cuaderno/config.toml — the vault’s configuration file.

cdno config <COMMAND> [OPTIONS]

These verbs deliberately do not open the vault. Opening one validates the config first, so a config that will not parse means no vault at all — and that is exactly when you need to read, check and fix it. Every other command would fail with a bare “loading config.toml”; these report the line, the column and the reason.

Commands

CommandWhat it does
showPrint the config verbatim — comments, key order and spacing exactly as on disk. --json adds the content hash.
validateRun the exact check Vault::new performs. Exits non-zero on a bad config. --file <PATH> checks a candidate without putting it in place.
editOpen the config in $EDITOR, then save it through the validate-first, compare-and-swap gate.
note-type set|removeAdd, change or remove a custom note type ([note_types.<name>]).
note-type installInstall a note type that ships with cdno (such as concept) into this vault. See Installing a bundled note type.
field set|removeAdd, change or remove a schema field ([schemas.<type>.fields.<field>]).
plot setSet how a declared tracking metric is plotted.
var set|removeAdd, change or remove a static template variable ([variables]).
prompt set|removeAdd, change or remove a prompted template variable ([variables.prompt]).

The save gate

Every write — edit and all the structured setters alike — goes through the same three steps:

  1. Validate first. A config that would not reopen is never written.
  2. Compare and swap. If the file changed on disk since it was read, the save is refused rather than clobbering the other edit.
  3. Write verbatim. Comments and key order survive.

So a refused save always leaves the file byte-identical. edit additionally keeps your rejected buffer and prints its path — a refused save must not also lose the work.

edit round-trips a scratch copy rather than opening config.toml itself: handing an editor the real file would route the write around all three steps. It needs a terminal, and refuses an editor that detaches instead of waiting, because there is then no moment at which the buffer is known to be written.

Setting a value merges, it does not replace

note-type set and field set change only the flags you pass. An omitted flag keeps its current value, so a one-flag edit cannot silently drop the rest of the entry.

To clear something, pass it empty (--template '', --required ''), or use the paired negative flag for a boolean (--no-append-only, --no-settable, --no-log-on-change, --no-required).

Changing a field’s --type is the one exception: default and values are declared against the old type, so they are dropped and the command says so. Re-set them in the same command or afterwards.

Installing a bundled note type

cdno ships with note types that cdno init declares in every new vault. Today there is one, concept. note-type install adds one to a vault created before it existed, so the older vault ends up exactly as a new one would:

cdno config note-type install --list            # what ships, and whether this vault has it
cdno config note-type install --name concept --dry-run
cdno config note-type install --name concept

It takes three steps, in this order, and each writes only what is absent:

  1. Template. Creates .cuaderno/templates/ if needed and writes the type’s template, but only when no file of that name exists. A template you have edited is never overwritten; the report says whether it matches the bundled one. If your declaration names a different template file, the bundled one is not installed.
  2. Folder. Creates the type’s folder (concepts/) if absent.
  3. Declaration. Appends the [note_types.<name>] block, comment included, through the same save gate as every other verb. If the config already declares the type, the declaration is never modified: it is compared with the bundled one, key by key, and the report names the note-type set command that would adopt each bundled value you might want.
declaration  written
template     written .cuaderno/templates/concept.md
folder       created concepts/

Re-running is safe. When there is nothing left to write, the report ends with concept: already installed, nothing to do and the command exits 0.

A config the vault would not open (check it with cdno config validate) is refused before anything is written, even when the type is already declared.

Two refusals are phrased in plain words, and in both nothing is left behind:

  • a config that declares note_types as an inline table cannot take an appended block; add it with cdno config edit;
  • another custom type already using the folder (concepts) is refused by the gate.

--list prints each bundled type’s purpose, folder, fields, template and its section headings, with this vault’s state: not installed, installed (matches), installed (declaration differs), installed (template customised) or installed (template missing). --dry-run prints the exact block and template that would be written, checks the result would validate, and writes nothing. With --json, the report is one object with changed, as the other verbs carry it:

{"changed": true, "note_type": "concept",
 "declaration": "written",
 "template": {"path": "templates/concept.md", "action": "written"},
 "folder": {"path": "concepts", "action": "created"}}

Once installed, a bundled type is an ordinary custom declaration: edit it with note-type set, or delete it with note-type remove.

Options

Promptable arguments are flags. Omit one in a terminal and you are asked for it; omit one with --no-interactive (or off a TTY) and you get a named error rather than a usage dump.

Common to every verb: the global options.

Examples

cdno config show                       # verbatim, pipeable back into the file
cdno config validate                   # exit 0 iff the vault would open
cdno config validate --file draft.toml # check a candidate first
cdno config edit                       # $EDITOR round trip through the gate

# Declare a custom note type, then extend it.
cdno config note-type set --name people --folder people --required name,email
cdno config note-type set --name people --folder humans          # keeps required + template
cdno config field set --note-type people --field email --type string --required

# Add the concept library to a vault created before it existed.
cdno config note-type install --name concept

# Template variables.
cdno config var set --name author --value "Your Name"
cdno config prompt set --name subject --message "What is this note about?"

# Plot a declared tracking metric.
cdno config plot set --activity gym --metric weight --plot line

See also

cdno watch

Watch the vault and reconcile the index whenever a note changes outside cdno.

cdno watch [OPTIONS]

Every other command reconciles once, at startup, and exits. That is enough when the process is short-lived, but it means a note you edit in another editor is invisible to search, lint and backlinks until the next cdno command happens to run. watch closes that gap: it stays in the foreground and reconciles on each debounced change.

Markdown on disk remains the source of truth — this only keeps the cache honest.

Options

Only the global options.

Behaviour

  • Foreground until Ctrl-C. There is no daemon mode; run it in a spare terminal or under your own process supervisor.
  • Debounced and coalesced. An editor’s atomic-save storm becomes one pass, and a bulk change (a git checkout across hundreds of notes) reconciles once rather than once per file.
  • Markdown only, outside .cuaderno/. Reconciliation reads every directory in the vault, and the filesystem reports those reads — so reacting to them would make each pass trigger the next. Restricting to .md files outside .cuaderno/ keeps the watcher from hearing itself.
  • A failed pass does not stop the watch. The usual cause is a file caught half-written; the next event reconciles again.

Each pass prints what changed, so an edit that did not land is visible as no index change.

Limitations

  • A .cuaderno/config.toml change needs a restart. Honouring a new ignore glob means rebuilding the vault rather than reconciling it. watch says so at startup rather than quietly using stale globs.
  • Interrupting a pass is safe but leaves the index incomplete until the next run — it is a cache, and the notes are untouched.

Examples

cdno watch                       # in the vault
cdno watch --vault ~/notebook    # or point it at one

See also

cdno frontmatter

Set typed frontmatter fields on a note through the index. Every other write surface (cdno log, section upserts) is body-oriented; this is the one that writes a frontmatter field, so a flag like the daily meds: true can be toggled without a hand-edit that would desync .cuaderno/index.db.

The field must be declared in .cuaderno/config.toml under [schemas.<type>.fields.<key>] and marked settable = true — the write is driven entirely by that spec (see the configuration reference).

cdno frontmatter set <note> <key> <value>

Set a declared, settable field to a new value.

cdno frontmatter set [OPTIONS] <NOTE> <KEY> <VALUE>

Arguments

ArgumentDescription
<NOTE>The note to edit: today, a YYYY-MM-DD date (both resolve to the daily note), or a vault-relative note path (e.g. projects/foo.md).
<KEY>The frontmatter field to set. Must be declared settable = true under [schemas.<type>.fields.<key>].
<VALUE>The new value, as a string. It is coerced to the field’s declared type (bool/int/float/string/date) and checked against any values allowed-set.

Takes only the global options. With --json, emits the { path, message } write result.

Rules

  • Declared + settable, default-deny. An undeclared key is rejected; a declared field without settable = true (absent or false) is rejected.
  • Type-checked. A value that doesn’t parse as the declared type — or isn’t one of a string field’s values — is rejected and nothing is written.
  • Reserved keys are blocked. type, status, a calendar type’s period key (date/week/month), and a project’s closed date are engine-owned regardless of config — use the lifecycle commands (cdno project park/activate/complete/drop, cdno question set-status, …) for those, so their auto-logging and index invariants are never bypassed.
  • No-op on no change. Setting a field to the value it already holds writes nothing and logs nothing.
  • Optional auto-log. When the field declares log_on_change = true, a real change stamps a key: old → new line into today’s daily note in the same commit.
  • Strict-exists (v1). The key must already be present in the note’s frontmatter (the daily flags exist via the template default). A missing key errors rather than being appended; ordered-insert is a planned follow-up. Slug resolution for projects/questions is likewise a follow-up — v1 resolves today/dates and explicit note paths.

Examples

cdno frontmatter set today meds true          # toggle today's daily meds flag
cdno frontmatter set today workout true        # if log_on_change, also logs it
cdno frontmatter set 2026-07-09 closed true    # a specific day's daily note
cdno frontmatter set projects/surrogate.md phase review

cdno note

Create, list and revise notes of a config-defined custom type (declared under [note_types.<name>] in .cuaderno/config.toml). Built-in types have their own verbs (cdno project create, cdno question create, …); this is the generic surface for custom types.

cdno note create <type>

Create a note of custom type <type>, written to <folder>/<slug(title)>.md.

cdno note create [OPTIONS] <TYPE> --title <TITLE>

Arguments

ArgumentDescription
<TYPE>A config-defined custom type (e.g. person). A built-in type is refused — use its own create command.

Options

FlagDescription
--title <TITLE>Required. The note’s title; its slug becomes the filename.
--slug <SLUG>Name the file yourself instead of deriving it from the title. Refused if invalid or taken; see the slug rule. Not combinable with --field slug=…: naming the note twice is refused (slug_given_twice), since the two could disagree.
--field <NAME=VALUE>A frontmatter field, repeatable. Each key must be a declared required/optional field of the type; every required field must be supplied.
--var <NAME=VALUE>A value for the type’s template prompted variable, repeatable.
--body-file <PATH>The note’s body, read from a file, without the title heading (the engine writes the # <title> H1; a leading # <title> line in the file is dropped). Written verbatim apart from leading and trailing blank lines and trailing whitespace. It fills the template’s {{body}} placeholder, or is inserted after the H1 when the template has none. When the template has {{body}} and nothing else fills it (--field body=, --var body= or a [variables] body in the config), it is opened in an editor if omitted interactively, and required under --no-interactive.
--origin <STRING>Where the note came from: one string of wikilinks, e.g. [[journal/2026/daily/2026-09-02#Woodbury identity]]. Written, trimmed, as the origin frontmatter string even when the template omits or does not quote {{origin}}, so the type must declare origin (the concept type does); otherwise the command is refused.

Plus the global options. With --json, emits a {path, message} result. If the type ships no template (.cuaderno/templates/<type>.md), a minimal note is synthesised from the declared fields plus a # <title> heading.

Creation is logged to today’s daily note as <type> created [[…]] — <title>; do not log it again by hand.

cdno note list <type>

List every note of custom type <type>, by path.

cdno note list <TYPE>

cdno note revise [note]

Refine a mutable custom note (such as a concept) in place. The revision is logged to today’s daily note as revised [[<path>]] — <reason>, or revised [[<path>#<Section>]] — <reason> for a section, in the same write; do not log it again by hand. Built-in note types (project, action, daily, …) and custom types declared append_only = true are refused.

cdno note revise [OPTIONS] [NOTE]

Arguments

ArgumentDescription
[NOTE]The note to revise, resolved as cdno open resolves a reference: a vault path with or without .md, a slug, or type:slug. An unknown or ambiguous reference is reported as cdno open reports it. Omitted in an interactive session, a picker offers every note.

Options

FlagDescription
--body-file <PATH>Replace the whole body (everything after the frontmatter, which is kept as it is) with the file’s contents, written verbatim. Cannot be combined with --section.
--section <STRING>The heading text of a section to upsert, without the # markers. Requires --content-file. An existing section is replaced together with its sub-sections; a missing one is appended as ## <section>.
--content-file <PATH>The section’s new text, without its heading. Requires --section.
--reason <STRING>Why the note was revised, in a short clause; it becomes the daily-log line.

Plus the global options. With neither --body-file nor --section, an interactive session opens the note’s current body in your editor, then asks for the reason and confirms before writing; under --no-interactive both are required. A note chosen in a picker (an omitted, ambiguous or unmatched reference) also brings the confirm, even when every flag was given. The note is read before anything is gathered, and a note that changed on disk after that read (an edit saved while the editor was open, say) is refused rather than overwritten: read it again and redo the revision.

Text identical to the note’s current content writes and logs nothing and prints No change to <path>. That includes closing the editor without changing anything: the reason and confirm are still asked for, and the result is No change to <path>. With --json, emits {path, message, changed, new_hash, log_line, section_target}.

cdno note rename [note]

Rename a note’s slug and follow every reference to it (RFC 0007): the wikilinks that resolve to it, the project: / portfolio: style fields that name it, an aliases: entry for the old slug on the note itself, and one renamed line in today’s daily log. The old name keeps resolving through the alias.

Renamable: projects, questions (a question whose slug a portfolio shares moves the portfolio with it), active action notes, portfolios and custom notes kept as single files. A portfolio with a same-slug question is renamed through the question. Dailies, weeklies, monthlies, evidence, tracking, inbox items, archived action notes, stewardships, commitments, a custom note kept as a folder and a note with no type are refused (note_not_renamable).

With --relink the note keeps its slug, and the links that still reach it through a former name are rewritten to the current one. An archived action note can be relinked, though not renamed.

Arguments

ArgumentDescription
[NOTE]The note to rename, resolved as cdno open resolves a reference: a path, a slug, a former slug or type:slug. Omitted in an interactive session, a picker offers the renamable notes only; so does the picker an ambiguous or mistyped reference opens (a question and its same-slug portfolio resolve to the question).

Options

FlagDescription
--to <SLUG>The new slug. Prompted in an interactive session; required under --no-interactive (unless --relink).
--relinkKeep the slug and rewrite every link that reaches the note only through a former name to it. Nothing moves. Cannot be combined with --to or --reason (a usage error, exit 2); composes with --dry-run and --skip-sync-check. See relinking the stragglers.
--reason <STRING>Why, in a short clause; it becomes the reason: line of the log entry. Never prompted.
--dry-runPrint the preview, writing nothing. Refused exactly as the write would be (see the sync check); exits 0 only when the write would proceed.
--skip-sync-checkRename even when the vault is behind its upstream. A git operation paused mid-way still refuses. See the sync check.

The preview

A long title makes a slug that stops mid-thought. This is a real run against a throwaway vault (see the tutorial for the whole story):

$ cdno project create --title "Calibrate the surrogate model against the high-fidelity thermal runs" --context work
Created projects/calibrate-the-surrogate-model-against-the.md

$ cdno action add --project calibrate-the-surrogate-model-against-the --title "Run the baseline sweep" --note --energy deep
Action added to projects/calibrate-the-surrogate-model-against-the.md with note actions/run-the-baseline-sweep.md

$ cdno action start --project calibrate-the-surrogate-model-against-the --query baseline
Started on calibrate-the-surrogate-model-against-the, logged to journal/2026/daily/2026-11-06.md

$ cdno note rename calibrate-the-surrogate-model-against-the --to surrogate-calibration --dry-run
Dry run: nothing written.
  move     projects/calibrate-the-surrogate-model-against-the.md -> projects/surrogate-calibration.md
  rewrite  actions/run-the-baseline-sweep.md                      1 field (project:)
  rewrite  journal/2026/daily/2026-11-06.md                       3 links
  alias    projects/surrogate-calibration.md gains `calibrate-the-surrogate-model-against-the`
  log      journal/2026/daily/2026-11-06.md
             renamed [[surrogate-calibration]] — Calibrate the surrogate model against the high-fidelity thermal runs
               was: calibrate-the-surrogate-model-against-the
  4 references in 2 notes · plan 2109…

$ cdno note rename calibrate-the-surrogate-model-against-the --to surrogate-calibration \
    --reason "the derived slug stopped mid-title"

The last command prints the same preview, with the reason: line (the reason is part of the plan, so its hash differs), and then Renamed `calibrate-the-surrogate-model-against-the` to `surrogate-calibration`. With the note and --to given on the command line the write runs straight away, in a terminal or not: nothing is prompted, so nothing is confirmed. Use --dry-run first to see the plan. Leave out --to in a terminal if you want to be asked for it, shown the preview and asked to confirm.

A rewrite line reads N links, M fields (project:, …) or both (2 links / 1 field (project:)). When the reference matched a former name, the first line is found by its former name `old` . The log block is the entry as it will be appended, without its time stamp. Warnings follow the totals line. In an interactive session, anything that was prompted (the note, the slug) brings the preview and a confirm. That confirmed write is given the preview’s plan hash within the same command: it writes exactly what was shown, or, if a referrer changed while the confirm waited, refuses with stale_plan and writes nothing. The hash does not join a --dry-run to a later run, so a change made between the two is not caught: a dry run is a look, not a lock. A rename to the note’s current slug writes nothing.

With --json: the plan (target, from, new_slug, moves, edits with their changes, references, notes, log_line, warnings, plan_hash) plus written (a bool), index_stale and dry_run. Refusals are rejection objects and exit 1; a rollback that itself failed (rename_incomplete) lists the paths to check by hand, one per line in text.

The sync check

A rename rewrites many notes at once, so in a vault that is a git repository synced with another clone it first checks that it is not about to rewrite notes the other clone has already changed. Unless --dry-run, it fetches the upstream (no credential prompt, at most 5 seconds; a fetch that fails or times out prints one warning: line and the check reads the upstream as last fetched). Then:

The vaultThe rename
is not a git repositorygoes ahead; no check
is a repository git cannot read (a timeout, a refused safe.directory)warns and goes ahead
has a git operation paused mid-way (a conflicted merge, a rebase, a cherry-pick)refuses with git_operation_in_progress; --skip-sync-check does not override it: finish or abort the operation first
is behind its upstreamrefuses with behind_upstream, naming the remedy: sync first (git pull, or your sync), or pass --skip-sync-check
has no upstream, or it cannot be readwarns and goes ahead
has uncommitted changesgoes ahead: a dirty tree is never a reason to refuse

The whole check runs again once the rename holds the vault’s write lock, with the same override and no fetch, so a merge started, or a fetch landed, while the confirm prompt waits is still refused, and nothing is written. A dry run is checked exactly as the write is, against the upstream as last fetched (it never fetches): it refuses with the same code and exit code, so --dry-run --skip-sync-check is how to see the plan of a vault that is behind. Warnings go to stderr; under --json they are warnings entries of kind sync_check in the plan instead (on stderr only before a refusal).

Refusals

Every refusal writes nothing. With --json each is a rejection object with the code below; in text the message is printed and the exit status is 1.

CodeWhyWhat to do
invalid_slug--to breaks the slug rule (upper case, spaces, a leading -, a value YAML reads as a number or boolean, too long).Pick a slug that follows the rule. It is refused, never rewritten.
slug_takenAnother note, a folder note’s folder or another note’s former name already holds the slug; the message names the holder.Pick another name. Nothing is suffixed.
note_not_renamableThe note is of a kind the rename does not take (see which notes can be renamed). The reason is one of seven, below.See below.
stem_sharedAnother note has the same file name, so a link to it cannot be told apart from a link to this one.Rename or move the other note first (or fix the duplicate), then rename.
reference_drift, reference_unaddressableRewriting a referrer’s frontmatter would change more than the reference, or a field names the note in a form the rewrite cannot address. The message names the note and the key.Fix that note by hand, then run the rename again.
stale_planA referrer changed while a confirm waited (any log append does it), so the plan you confirmed is no longer the plan. Over MCP the same code answers a write whose expected_plan no longer matches.Preview again and confirm the new plan.
behind_upstreamThe vault is behind its upstream. The text is the vault is N commits behind its upstream; sync the vault first (pull the upstream's commits, e.g. with git pull), or pass –skip-sync-check to rename anyway.Sync first, or pass --skip-sync-check. See the sync check.
git_operation_in_progressA merge, rebase or cherry-pick was left paused. Text: a git operation is in progress in the vault (…); finish or abort it, then rename.Finish or abort it. --skip-sync-check does not override this.
rename_incompleteA write failed and the rollback could not restore every file; the paths are listed.Check those paths by hand, then run the rename again.

The seven note_not_renamable reasons, and what to do for each. In text mode the code itself is not printed, only the message (note '…' cannot be renamed: <reason>); --json carries both.

reasonMeaningWhat to do
type <name> (type none for a note with no type:)The kind never renames: daily, weekly, monthly, commitment, stewardship, evidence, tracking, inbox; or a note without a type.Nothing: the note keeps its name. For a note with no type, add one first.
archivedAn archived action note, whose frozen prefix would change. A --relink is not refused: it changes nothing on the note.Nothing: the note keeps its name. --relink still repairs the links to it.
folderA folder note other than a portfolio’s portfolios/<slug>/_index.md, of any type (a custom note kept as a folder, projects/<slug>/_index.md); or a portfolio kept as a single file. A portfolio on its own, in its folder, renames fine.Nothing: the note keeps its name.
rename the questionA portfolio whose slug a question shares (the portfolio alone is refused). A --relink of it is refused too, and relinking the question covers the question’s former names only; a rename never gives the pair’s portfolio aliases, so this matters only for one listed by hand.Rename the question: the pair moves together.
it has no frontmatter to record its old slug inThe note has no frontmatter block.Repair the note (add frontmatter), then rename.
its `aliases:` is not a list the old slug can be added toaliases: is a scalar or some other shape.Make aliases: a list, then rename.
its `aliases:` lists the new slug in a form it cannot be removed fromThe note takes back a former name that aliases: lists twice or with a tag.Tidy the aliases: list by hand, then rename.

A rename that would write over a file the vault already holds is refused as slug_taken. That includes a rename that changes only the case of a slug (Alpha to alpha): it is refused where the filesystem cannot tell the two names apart (macOS and Windows by default) and allowed where it can (a default Linux filesystem). The message names the note itself as the holder (slug 'alpha' is taken by 'projects/Alpha.md' (as its stem)), which is expected. To change only the case, rename through a temporary slug (Alpha to alpha-tmp, then alpha-tmp to alpha); the note ends up with both old names as aliases. The old spelling (Alpha) stays listed but nothing resolves it, because aliases must be slug-shaped; it can be deleted by hand.

Renaming back, and former names

The old slug is kept on the note as aliases: [<old>], and no other note may take it while it is listed. Renaming old to new and then new back to old is allowed, and leaves aliases: [new]. In the log, the first renamed line’s head link is rewritten to [[old]], like any other link to the note, while its was: old line stays as written: was: is a copied value, never a link, so it is the record of what the note was called at the time. The second line reads renamed [[old]] — … was: new.

A command that is given a former name still finds the note. cdno open, cdno note revise and the rename preview say so (found by its former name `old` ). cdno project show does not: it prints the note’s current slug in its header, which is how to learn the new name there.

A link written with an old name still works in cdno, so leaving it is harmless. Editing it by hand to the new name is allowed too, even in a past log entry or an archived action note: it is the same address correction a rename makes.

A link written with a former name after the rename is a straggler: cdno still resolves it through the alias, and cdno lint reports it, but an editor cannot follow it. The usual way one appears is a vault synced between two machines with a journal merged line by line (merge=union): the rename rewrote a log line on one machine, the other appended a line beside it, and the merge kept both versions, so the old line comes back holding the old name (the merge keeps the whole hunk, so usually several lines come back, not one). --relink rewrites every straggler in one go:

$ cdno lint
[warning] journal/2026/daily/2026-11-06.md: `[[calibrate-the-surrogate-model-against-the]]` names a renamed note — now `surrogate-calibration`

$ cdno note rename surrogate-calibration --relink --dry-run
Dry run: nothing written.
  rewrite  journal/2026/daily/2026-11-06.md  1 link
  log      journal/2026/daily/2026-11-06.md
             relinked [[surrogate-calibration]] — Calibrate the surrogate model against the high-fidelity thermal runs
               references: 1
  1 reference in 1 note · plan 1c09…

$ cdno note rename surrogate-calibration --relink

The last command prints the same preview and Relinked 1 reference to `surrogate-calibration`. It goes through the same preview, plan hash, sync check and refusals as a rename, with these differences:

  • Only a reference through a former name is rewritten, links and fields alike. A link is rewritten when it resolves to the note through one of its aliases (the links cdno lint warns about), and only to an address that resolves to the same note. A link that already names the note by its slug or path is left alone, and so is a link a former name no longer reaches (another note took that file name since). A reference can only ever move to where it already points. A field such as project: that holds one of the note’s former names is rewritten too (lint does not report these, since a field is not a link): a field is read by its value alone, so until the relink it named no note at all.
  • It corrects addresses; it does not tidy the merge. The lines the union merge duplicated stay after the relink, now with identical text twice. They are the merge’s, not journal entries, so deleting the extra copy by hand is yours to do if you want it.
  • An archived action note can be relinked. A rename refuses it (archived, its frozen prefix would change), but a relink changes nothing on the target, so the links to a completed action that was renamed before it was archived can be repaired. Every other refusal of a rename applies: note_not_renamable for the other reasons, stem_shared, the referrers’ own and the sync check.
  • Nothing moves and no alias changes. The note stays where it is, byte for byte unless it links to itself by a former name, and its aliases: stay listed, so an old link anywhere else (a file ignore hides, the other machine) keeps working.
  • The log line is relinked. It reads relinked [[<slug>]] — <title> with an indented references: <n> line and no was:, since the name did not change. With no straggler there is nothing to write: Nothing to relink: no link reaches `<slug>` through a former name., and nothing is logged.

With --json the shape is a rename’s, with moves empty and new_slug equal to from.

On the other machine

Some vaults live in a git repository with a working copy on more than one machine, synced by a tool that merges. Three things apply:

  • Rename on a machine that is up to date. The sync check above refuses a rename while the vault is behind its upstream. Your sync tool carries the result to the other machine on its next run. Use --skip-sync-check only when you know the other machine has nothing unpushed that touches these notes, for instance when the upstream is unreachable and you accept the risk.
  • A conflict is the other machine’s edit meeting the rename. The sync stops rather than resolve it. Resolve it as you would any merge, then run cdno lint: a link the merge brought back with the old name is reported as a straggler ([[old]] names a renamed note — now new). It still resolves in cdno, through the alias, but an editor cannot follow it: run cdno note rename <note> --relink to rewrite every one to the current name (or fix one by hand).
  • Run any cdno command in the vault once on every clone, on a cdno that stamps RLM 1.2 (one that has note rename), before renaming anything an archived action references. (Starting the MCP server on it counts too.) That records each archived action note in a form that ignores link addresses, which is how cdno lint tells a corrected link from an edit. A clone that receives a renamed archived note before it has done so reports an archived action note error from cdno lint (modified an existing line, or truncated below its frozen prefix after a shorter slug) that names cdno reindex. cdno reindex clears it, at the cost of the stored records: every archived action note then loses that check.

Examples

cdno note create person --title "Ada Lovelace" --field name=Ada --field role=advisor
cdno note create person --title "Ada Lovelace, programmer and mathematician" --field name=Ada --slug ada-lovelace
cdno note list person
cdno note rename calibrate-the-surrogate-model --to surrogate-calibration --dry-run
cdno note rename calibrate-the-surrogate-model --to surrogate-calibration --reason "the derived slug was too long"
cdno note rename calibrate-the-surrogate-model --to surrogate-calibration --dry-run --skip-sync-check   # plan a vault that is behind
cdno note rename surrogate-calibration --relink --dry-run   # the links still under a former name
cdno note rename surrogate-calibration --relink
cdno note revise concept:woodbury-identity --section Proof --content-file proof.md \
  --reason "shorter proof via push-through"

cdno commit

Manage standalone commitments — dated promises kept as their own notes. To view all commitments (from every source), use cdno commitments.

cdno commit [OPTIONS] <COMMAND>

Subcommands

SubcommandDescription
createCreate an active commitment at commitments/<slug>.md
doneMark a commitment completed and archive it
dropEnd a commitment that was not kept
rescheduleMove an active commitment’s due date

All four honour --json ({path, message}, non-interactive).


cdno commit create

Create an active commitment note. Optionally attribute it to a project or stewardship.

FlagDescription
--title <TITLE>Commitment title (the slug derives from it).
--slug <SLUG>Name the commitment’s file yourself instead of deriving it from the title. Refused if invalid or taken; see the slug rule.
--due <YYYY-MM-DD>Deadline.
--context <CONTEXT>Life domain (work, personal, …).
--project <SLUG>Optional associated project.
--stewardship <SLUG>Optional associated stewardship.
--var <NAME=VALUE>Value for a custom template’s prompted variable ([variables.prompt]). Repeatable. See Prompted variables.
cdno commit create --title "Pay rent" --due 2026-06-01 --context personal
cdno commit create --title "Review Erik's draft" --due 2026-05-20 --context work --project projects/icml-paper
cdno commit create --title "Send the surrogate-model results to the steering group" --due 2026-12-01 --context work --slug send-results

cdno commit done

Mark a commitment completed: stamps status and completed, and moves the note to commitments/_done/<year>/<slug>.md.

FlagDescription
--slug <SLUG>Commitment slug.
cdno commit done --slug pay-rent

cdno commit drop

End an active commitment that was not kept — cancelled, superseded, or overtaken by events.

FlagDescription
--slug <SLUG>Slug of the active commitment.
--reason <TEXT>Why it ended. Optional, and never prompted for.
cdno commit drop --slug quarterly-report
cdno commit drop --slug quarterly-report --reason "the client cancelled the engagement"

Use this rather than done when the promise was not fulfilled. done writes commitment completed [[slug]] into the daily log, which every weekly and monthly review reads back from, so using it for a cancelled promise makes the vault assert something nobody did. Deleting the note is not the alternative either: that destroys the record that the promise was ever made, and desyncs the index on the way out.

The note is archived to commitments/_done/<year>/ like a completion, stamped status: dropped with completed cleared — so it never appears as completed work. The log entry says what happened, with the reason on its own line:

- **16:30**: commitment dropped on [[quarterly-report]] — Quarterly report
  reason: the client cancelled the engagement

A dropped commitment can afterwards be neither completed nor rescheduled. Nothing is destroyed — the note keeps its body and its created date — but a drop is re-decided rather than undone: if the promise comes back, make it again, and the record shows both decisions.

cdno commit reschedule

Move an active commitment’s due date, recording the move in today’s daily note.

FlagDescription
--slug <SLUG>Slug of the active commitment.
--due <YYYY-MM-DD>The new due date. Must differ from the current one.
cdno commit reschedule --slug quarterly-report --due 2026-06-15

Commitments slip, and that is normal rather than an error state. Use this instead of deleting and recreating the note: recreating destroys the body — the notes on who was chased and what was promised — and resets created, the one field that shows how long something has been slipping.

The log entry carries both dates:

- **09:30**: commitment rescheduled on [[quarterly-report]] — Quarterly report
  was: 2026-06-01
  now: 2026-06-15

That is what makes repeated slippage visible instead of silently rewritten: a commitment moving once is a checkpoint, a commitment moving for the fourth time is a signal. Moving a date earlier is allowed; moving it to the date it already carries is refused, since the entry would assert a slip that never happened.

create_commitment, complete_commitment, drop_commitment, reschedule_commitment. (View via get_commitments.)

See also

cdno lint

Validate every indexed note and report what is wrong with it — frontmatter, links, attachment pairing, and lines the canonical parsers silently skip. Errors fail the command; warnings (such as broken wikilinks) are non-fatal unless --strict is given.

A wikilink or embed that points at an attachment — a pasted image, a filed PDF — is not a broken link: attachments are never indexed, but the target is resolved against the filesystem (relative to the linking note, then to the vault root) before a link is called broken. Only a target that matches nothing at all is reported, and a missing ![[embed]] reads as a missing file rather than a link that “resolves to no note”.

A link that resolves only through a note’s aliases: entry — a former name — is a straggler: it still works, but lint warns and names the note’s current slug ([[old]] names a renamed note — now new), so you can fix the link by hand, or rewrite them all with cdno note rename <note> --relink. Lint also warns when an alias collides with another note’s name. If it equals another note’s file stem, the stem wins and the alias can never be followed (alias x on <path> is shadowed by <other>). If it equals the folder name of a folder note such as portfolios/x/_index.md, the alias takes the bare link [[x]] (alias x on <path> takes bare [[x]] from folder note <other>); the folder form [[portfolios/x]] still reaches the folder note. A straggler to a folder note is told to use the folder form, since a bare link to the new name would not resolve.

It also reports lines that were plainly meant to be structured but will never be read as such, because the parsers that consume them skip what they cannot parse rather than complaining. That covers malformed ## Active Habits and ## Periodic Commitments bullets on a stewardship dashboard, and — in a daily note’s ## Logs — a start or close marker that cdno now will not see:

[warning] journal/2026/daily/2026-09-15.md: log line `- **09:30**: started [[alpha]] - Draft methods (deep)` reads as a `started` marker but `cdno now` will not see it -- found an ASCII hyphen (-) where an em-dash (—) separates the slug from the action

The shape has to be - **HH:MM**: started [[slug]] — text, and every part of it matters: the - bullet and the stamp are what make the line a log entry at all, and the separator must be a real em dash (U+2014). A stamp that was attempted and mangled — - 09:20: unbolded, - **25:99**: out of range, - **09:40** with no colon — is caught too, not only one that is missing outright. The check is deliberately narrow — the marker has to open the entry and be followed immediately by [[ — so ordinary prose in ## Logs, including a sentence that merely mentions starting something or names a note mid-sentence, is never flagged. The renamed [[new]] — Title line a rename writes, and the relinked [[new]] — Title line of --relink, get the same near-miss check (neither is a focus marker, so cdno now never reads them), and a was: continuation under it that contains a [[ link is a warning, because the value is a copied slug and a link would be rewritten by the next rename.

It checks that an archived action note (actions/_done/<year>/) has only grown since it was archived. The archival recorded the note’s frozen prefix; lines appended after it (a late retrospective) are fine, while an edit to an existing line, or a cut into the prefix, is an error:

[error] actions/_done/2026/characterise.md: archived action note modified an existing line (append-only after completion)

A rename may change the prefix in one way only: it re-targets references, rewriting the address of a wikilink ([[old]] becomes [[surrogate-model]]) and a reference field such as project:. So the prefix is accepted when either its bytes are unchanged or they match with every wikilink address and reference-field value set aside. The cost of that rule is that a hand edit which only re-targets a reference inside a frozen prefix is no longer detected; any other change still is. This holds on every clone that opened the vault once on this version with the backfill completing (it is retried at every open, and a lock timeout only postpones it) before the renamed file arrived: a note archived on an older version and renamed elsewhere first is reported as an error that names cdno reindex, which clears it by rebuilding the index without any archival snapshots, so every archived note loses this check. On a renamed note, a hand addition that follows an unmatched backtick at the end of the prefix with no blank line in between is reported too; start a late addition after a blank line. A note archived by a newer cdno, whose snapshot this cdno cannot read in the second form, gets a warning naming the version, and only the unchanged-bytes form is checked for it.

It checks each project map’s status against the folder the map sits in and against its closed: date. A status its folder contradicts is an error, since the project verbs find a map by its folder and act on it by its status; the message says which way to repair it:

[error] projects/_parked/alpha.md: project says `status: active` but is not at `projects/<slug>.md`; move it there, or set the status its folder stands for

A completed or dropped map outside projects/_done/<year>/ is told to move there and set closed: <date>, or to set status: active and close it with cdno project complete. A closed: date that disagrees with the status is a warning: a completed or dropped map without one cannot be placed by the reviews, and an active or parked map carrying one should have closed: null.

It reads each open map’s held_by: (the name of whoever else is moving it) against the vault’s [projects] holder_sentinels list (see configuration). A value that is not a plain name, such as an unquoted wikilink, is an error, because the holder you wrote is not honoured and the map still counts toward the cap (or would, once a parked map is activated). For an unquoted wikilink the message says to quote it; for anything else (a number, a date, a list) it says to write a single quoted name:

[error] projects/harbour.md: project `held_by:` is not a plain name, so the holder is not honoured and the map counts toward the cap; quote it: `held_by: "[[people/ana]]"`

A value on the list, or a blank one, names you or nobody, so the map is yours and counts. That is a warning:

[warning] projects/harbour.md: project `held_by: "tbd"` does not name someone else: delete the line, or name the holder; a holder that names you or nobody means the map is yours

And it warns, once, on .cuaderno/config.toml when the projects that count (active and naming no other holder) exceed max_active_projects, listing them. The project verbs refuse to go over the cap, so this catches a cap lowered in config or maps edited by hand:

[warning] .cuaderno/config.toml: 6 projects count toward the cap of 5 (`max_active_projects`): [[alpha]], [[bravo]], [[charlie]], [[delta]], [[echo]], [[foxtrot]]; park or close one until no more than 5 count

If a project map cannot be read at all (an unknown status, a missing context or created), the count is unknown, and the same file gets an error saying so: could not count the projects against the cap: …. The project verbs fail on such a map too, so fix it first.

It also warns when a customised template (.cuaderno/templates/<type>.md) lacks a frontmatter key that the built-in template has, typically one a release added after you customised it, and reports a customised template it cannot read (for instance one that is not UTF-8) as an error:

[warning] .cuaderno/templates/project.md: custom template `project` lacks `closed` (run `cdno templates sync project`)

cdno templates sync adds the key without touching your edits. Lint cannot tell a new key from one you removed on purpose, so a built-in key you deleted from your template is reported too.

It reads [vault] method_version (see configuration) and reports, on .cuaderno/config.toml, a vault this cdno would not write to as an error: one on an older major version (the message points at cdno migrate --dry-run, then cdno migrate) or a newer major (upgrade cdno). A vault on a newer minor version is a warning, since writes proceed:

[warning] .cuaderno/config.toml: this vault follows RLM 1.3, a newer minor version than the RLM 1.2 this cdno implements — writes proceed; upgrade cdno for the practices it adds

A vault on this cdno’s version, or on an older minor one (an unstamped vault included), gets no row here. A malformed method_version ("v1", "1.1.0") is not a lint row: it is a configuration error, so the vault does not open and every command, lint included, fails with an error naming the key. The one exception is a long-running server (cdno-mcp-server) whose config was edited to a malformed value after it opened: it keeps running, and there lint (and the MCP lint tool) reports one error row on .cuaderno/config.toml naming the key instead of aborting. Fix the value (see configuration).

While the vault records a method version below RLM 1.1 (an unstamped vault reads as 1.0), it gives one warning per overdue hard date on a parked project map, on every run, since the commitments register keeps those dates as of RLM 1.1: the one-time read-through of parked deadlines. The row sits on the map for a hard milestone and on the action note for a due:, and names the fix:

[warning] projects/_parked/beta.md: hard milestone `Grant report` (2026-04-15) on parked project [[beta]] is overdue and in the commitments register again (a parked map's hard dates are promises under RLM 1.1): drop it with `cdno project milestone drop`, or re-date it on the map; this read-through row disappears once the vault records RLM 1.1, which `cdno migrate` records

An action note’s row says to re-date its due: in the note, or drop the action with cdno action drop. Future dates are not warned on; cdno migrate --dry-run lists them all. Once the vault records RLM 1.1 the rows are gone for good.

cdno lint [OPTIONS]

Options

FlagDescription
--strictTreat warnings as failures too (exit non-zero on any issue).

Plus the global options. lint ignores --json.

Exit status

  • Clean, or warnings only without --strict → exit 0.
  • Any error (e.g. unknown note type, invalid frontmatter) → non-zero.
  • With --strict, any warning also → non-zero. Useful in CI to keep a vault pristine.

Examples

cdno lint                 # report issues; fail only on errors
cdno lint --strict        # fail on warnings too (e.g. broken links)

lint.

See also

cdno reindex

Rebuild the SQLite index from scratch off the Markdown source of truth. The recovery path for a corrupt or stale index.

cdno reindex [OPTIONS]

Options

Only the global options. reindex ignores --json.

When you need it

Almost never — Cuaderno reconciles the index automatically on every run (see Business rules). Reach for reindex when:

  • search or link results look wrong after a large external edit or a sync conflict, or
  • you deleted .cuaderno/index.db and want to rebuild it eagerly rather than on next use.

Because the Markdown files are authoritative, a full rebuild loses no note. It does drop one thing the Markdown cannot rebuild: the archival snapshots, so every archived action note loses the frozen-prefix check cdno lint runs on it.

Examples

cdno reindex

See also

cdno migrate

Record the version of the Research Logbook Method this cdno implements in the vault ([vault] method_version in .cuaderno/config.toml), running any file migrations that version needs and logging the move in today’s daily note.

cdno migrate [OPTIONS]

Options

FlagMeaning
--dry-runPrint what would change, and write nothing.

Plus the global options. --json prints the same report as an object: dry_run, outcome (planned, migrated or nothing_to_do), vault (null when unstamped), vault_described, binary, migrations, config_line, log_entry and read_through.

migrate asks nothing and confirms nothing: run it with --dry-run first to see exactly what will change.

What it does

  • This cdno implements RLM 1.2. A vault without a recorded version is unstamped and read as RLM 1.0; cdno migrate stamps it, or a vault stamped 1.0, with 1.2.

  • File migrations, when a version needs any, never touch the journal or any append-only note. When there are none the report says none: additive.

  • The daily log gains one entry:

    vault migrated
      was: unstamped (read as 1.0)
      now: 1.2
    
  • A vault below RLM 1.1 gets a read-through, whether or not it is stamped: every hard date on a parked map (a hard milestone, or an action note’s due:), which the commitments register now keeps, listed with its map, overdue first. Drop or re-date each; cdno lint warns on the overdue ones until the vault records RLM 1.1.

      read-through (hard dates on parked maps, overdue first; drop or re-date each):
        2026-04-15  Grant report  (hard milestone on [[beta]], overdue)
        2026-05-01  Shelved draft  (action due on [[beta]], overdue)
        2026-11-01  Final review  (hard milestone on [[beta]])
    
  • A vault already at this cdno’s version prints vault is at RLM 1.2; nothing to do: its read-through is done.

  • A vault on a newer version than this cdno is refused with a non-zero exit: upgrade cdno instead.

migrate runs only when you run it: never when a vault opens, and never over MCP.

The notice

When a vault records an older method version than this cdno, other commands print one line on stderr after they finish, naming both versions and pointing at cdno migrate --dry-run. A vault on a newer minor version gets a line saying writes proceed and cdno can be upgraded. migrate, init, now --line and shell completion never print it, and stdout (including --json) is never touched.

Examples

cdno migrate --dry-run
cdno migrate

See also

cdno normalise

Reorder note frontmatter into the canonical key order of each note’s template (a custom .cuaderno/templates/ override if present, else the built-in). Notes cdno creates are already canonical; this fixes hand-authored or migrated notes.

cdno normalise [OPTIONS]

Options

FlagDescription
--checkReport out-of-order notes without rewriting them. Exits non-zero if any are out of order.

Plus the global options. normalise ignores --json.

Examples

cdno normalise            # rewrite notes into canonical frontmatter order
cdno normalise --check    # report only; non-zero exit if anything is out of order (CI-friendly)

normalise only reorders existing keys — it never changes values or adds/removes fields. The canonical order is whatever the matching template defines.

See also

cdno completions

Print a shell-completion script. Source it in your shell’s rc file. The script wires vault-aware dynamic suggestions — --project, --portfolio, --stewardship, --slug, etc. are completed by re-invoking the binary when you press TAB.

cdno completions [OPTIONS] <SHELL>

Arguments

ArgumentDescription
<SHELL>Target shell. One of bash, zsh, fish, elvish, powershell.

Options

Only the global options.

Setup per shell

# zsh — in ~/.zshrc:
source <(cdno completions zsh)

# bash — in ~/.bashrc:
source <(cdno completions bash)

# fish — write it into the completions dir:
cdno completions fish > ~/.config/fish/completions/cdno.fish

# elvish / powershell: emit the script and source it per that shell's convention.
cdno completions powershell

After reloading your shell, TAB completes commands, flags, and live vault values (project slugs, portfolio slugs, …).

See also

MCP server reference

cdno-mcp is a Model Context Protocol server that exposes your vault to AI clients (Claude Desktop, Claude Code, Kiro, Gemini CLI, …). It runs the same domain engine as the CLI, so anything the assistant does goes through the same rules and lands in the same Markdown files. To wire it up, see Connect to Claude.

Transport and vault selection

Two binaries serve the same tool catalogue:

  • cdno-mcp speaks JSON-RPC over stdio — the client launches it as a subprocess. It opens the vault named by CUADERNO_VAULT_PATH, or, if that’s unset, discovers one from its working directory (the same rule as the CLI).
  • cdno-mcp-server speaks MCP Streamable HTTP for remote clients — see The HTTP server for its flags and security model.

The tool surface

The server advertises 66 tools. This reference groups them by purpose:

GroupPageWhat’s in it
Context-gathering readsContext-gathering toolsOrientation, project/portfolio/weekly context, search, reads, lint, triage list
WritesWrite toolsLog, capture, file evidence, project/action/milestone/waiting edits, note revision and renaming, commitments, tracking, daily/weekly sections
Creation & lifecycleCreation and lifecycle toolsCreate projects/portfolios/questions/stewardships, link portfolios, park/activate/complete/drop, status transitions

Every tool returns typed JSON; the shapes mirror the CLI’s --json output, so a client gets the same structures whichever surface it uses.

Focus

Four of the write tools (pause_action, switch_action, switch_unplanned_action, resume_action) move the one “what I am on” slot that current_focus reads, and eighteen write results carry a focus field so the focus rides along without a second call. The server never refuses a write for being outside the focus; it refuses a second start (or a switch to the focused action, or a resume_action over a different focus) with a coded focus_open rejection, and pause_action or resume_action with nothing to act on with no_focus. See Write tools and Focus.

Conventions

  • Slugs, not paths. Tools take slugs (surrogate-model), matching the CLI.
  • Substring matching for completing actions/milestones and resolving waiting-on items, exactly as on the CLI.
  • The same rules apply. The five-project cap, append-only notes, auto-logged project-state history, and commitments aggregation all hold — the MCP server is not a back door around them.

When a call is rejected

Cuaderno splits failures along the line MCP itself draws, and the distinction is worth knowing because the two arrive in different places in the response.

A rejection you can act on comes back as a tool result with isError: true — not as a JSON-RPC error. The response carries no error member at all, and the payload is the single text content item, JSON-encoded exactly as a successful result’s payload is:

{ "isError": true, "content": [{ "type": "text", "text": "…" }] }

Decoding that text gives the rejection itself. This is real output from complete_action with a query matching two bullets:

{
  "code": "ambiguous_action",
  "details": {
    "candidates": [
      "Draft the methods section (deep)",
      "Revise the methods appendix (light)"
    ],
    "query": "methods",
    "slug": "ablation-study"
  },
  "message": "ambiguous action match for 'methods' on project 'ablation-study': [\"Draft the methods section (deep)\", \"Revise the methods appendix (light)\"]"
}

Three fields, and each has a job:

FieldFor
codeA stable snake_case discriminant to branch on — ambiguous_action, state_too_long, project_cap_reached, field_not_settable, …
messageThe human-readable sentence, for showing a person
detailsThe structured fields needed to recover — candidates to pick from, chars/max to condense against, active_projects to park one of

details is the part that matters for automation. An ambiguous query hands back its candidates as an array, so a client picks from a list rather than parsing them out of a sentence.

Five project codes carry details a client is likely to branch on:

CodedetailsWhen
project_not_activeslug, status (parked, completed or dropped), and closed when the map carries a dateA parked status means a focus verb (start_action, resume_action, switch_action, start_unplanned_action, switch_unplanned_action) or park_project was refused: starting or picking up work needs an active map, so activate it first only if the user wants to work on it now. Every other project-body verb (add_action, update_project_state, add_milestone, …) works on a parked map. A closed status means a body verb on a completed or dropped project, or a close repeating the outcome it already has: the project is finished, unless the user wants it back.
project_cap_reachedcurrent, max, active_projectsA project would begin to count with the counted projects at the cap (activate_project, or taking back an active project with hand_off_project). current and active_projects are the projects that count, active and held by no one else, so each listed slug is one whose parking, completion or dropping frees a slot: offer those, or leave the project as it is.
ambiguous_projectslug, candidates: [{path, note_type}]The slug names maps in more than one of projects/, projects/_parked/ and projects/_done/<year>/. Nothing can pick for you: keep one of the files by hand.
project_in_focusslug, actionA project was to be handed off while action, one of its actions, is in focus: a project is not handed off while you are in the middle of its work. Pause the action first, if the person wants to hand it off now.
empty_field (for held_by)field: "held_by", value, remedy: "take_back"The holder given names the person or nobody (me, tbd, someone, …, the vault’s [projects] holder_sentinels). If they cannot name who has the project, it is theirs: take it back rather than hand it off. Any other empty_field carries field alone.

The test is simply whether a different argument would change the outcome. A slug that matches no note is a rejection (not_found), and its message carries the list of slugs that do exist. A note missing the section a tool writes into is a rejection. So is a name that is already taken.

A mechanical failure stays a JSON-RPC error (-32603): a disk that would not write, permission denied, a contended write lock, an index that would not answer, a transaction that rolled back. Nothing the caller passed can fix those, so there is nothing to branch on. Malformed calls — an unknown tool, unparseable arguments — are protocol errors too (-32602), per the spec.

The practical upshot for a client: read isError on the result, not just the presence of error. A rejection is a normal, expected answer to a call that was itself well-formed.

Slug rejections

An explicit slug on a create tool (Creation and lifecycle) is refused with one of four codes, each leaving the vault untouched. They are the same objects the CLI prints under --json.

CodedetailsWhen
invalid_slugslug, whyThe slug breaks the shape rule (lowercase a-z, 0-9, single -, at most 50 characters, not YAML-typed); why names the part. Fix the shape, or ask the person for another name.
slug_takenslug, holder, kind (stem|folder|alias)Another note already holds the name. If holder is the note you meant, use it; otherwise ask the person for another name. alias means the name is a former name another note still lists. Do not suffix it yourself.
portfolio_slug_mismatchquestion_slug, slugcreate_portfolio with question set to a question’s slug and a slug that is not that slug. Drop slug or pass the question’s.
slug_given_twicefieldcreate_custom_note with slug and a slug entry in fields. Pass one of them.

Building skills on top

Multi-step rituals (a morning orientation, a guided weekly review) are best wrapped as Claude skills that call these tools in sequence. See Using with Claude skills.

The HTTP server

cdno-mcp-server serves the same tool catalogue as the stdio cdno-mcp, over the MCP Streamable HTTP transport, for clients that reach your vault remotely — most notably Claude’s custom-connector infrastructure, which connects from Anthropic’s cloud for every surface (web, desktop, mobile).

cdno-mcp-server --vault ~/vault              # listens on 127.0.0.1:8787, endpoint /mcp

Security model — read this first

The binary issues no OAuth of its own, on purpose. Static bearer tokens are not spec-legal for remote MCP connectors; real deployments terminate OAuth 2.1 at an identity-aware proxy (for example Cloudflare Access with Managed OAuth in front of a Cloudflare Tunnel). The server’s own contribution is origin-side validation of the identity assertion the proxy injects (Cf-Access-Jwt-Assertion): RS256 against the team’s JWKS, strict issuer/audience/expiry, fail closed — configure it with CDNO_ACCESS_TEAM_URL and CDNO_ACCESS_AUD. If the JWKS cannot be fetched at startup, the server refuses to start rather than serve unauthenticated.

Without that configuration, cdno-mcp-server refuses to bind anything but loopback. Be precise about what that guarantees: the process only accepts connections arriving on its own loopback interface. It cannot detect a tunnel or SSH forward that bridges the port outward — never bridge this port without the authenticating proxy in front. The server logs a warning at startup to the same effect whenever it serves real vault data unauthenticated. Configuring the JWT validation is exactly what lifts the non-loopback restriction (e.g. binding 0.0.0.0 inside a container).

Flags

FlagEnvDefaultPurpose
--vault <path>CUADERNO_VAULT_PATHcwdVault root
--bind <addr>CDNO_MCP_BIND127.0.0.1:8787Listen address (non-loopback refused until #302)
--allowed-host <host>CDNO_MCP_ALLOWED_HOSTS (comma-separated)—Extra Host header values to accept on top of the loopback defaults (DNS-rebinding protection). A public deployment adds its hostname
--smoke—offServe a single echo tool holding no vault handle — prove tunnel/auth infrastructure end-to-end with zero vault exposure
--read-only—offAdvertise only the context-gathering read tools; mutating tools are absent from the dispatch table entirely
--reconcile-interval-secs <n>CDNO_MCP_RECONCILE_INTERVAL_SECS300Periodic index reconciliation; 0 disables
--git-checkpoint-interval-secs <n>CDNO_MCP_GIT_CHECKPOINT_INTERVAL_SECS60How often the git sweep runs. 0 disables it; warns and no-ops when the vault isn’t a git repo
--git-checkpoint-mode <mode>CDNO_MCP_GIT_CHECKPOINT_MODEcommitWhat the sweep does with a dirty tree: commit here, or nudge-only — see The recovery trail
--sync-nudgeCDNO_MCP_SYNC_NUDGEoffTouch a sentinel file after every verified write so an external sync agent reacts at once instead of on its own timer — see Pairing with a sync agent
--sync-nudge-path <path>CDNO_MCP_SYNC_NUDGE_PATH<vault>/.git/cdno-sync.nudgeWhere that sentinel lives. Setting it does not by itself enable nudging
--access-team-url <url>CDNO_ACCESS_TEAM_URL—Cloudflare Access team URL (JWT issuer + JWKS host). Requires --access-aud; activates origin JWT validation and lifts the loopback-only restriction
--access-aud <tag>CDNO_ACCESS_AUD—The Access application’s AUD tag (expected aud claim). Requires --access-team-url

The recovery trail

Exposing write tools remotely means anything a confused or prompt-injected session does lands in your vault. The damage limit is that every mutation ends up in a git commit you can diff and revert. The sweep is what provides it: on an interval it takes the vault write lock, and if the tree is dirty it commits everything as cdno-mcp checkpoint (N path(s)). It is a sweep rather than a per-write hook, so out-of-band edits — the CLI, your editor, a sync tool — join the trail too.

What must hold is that something commits. Which actor does is configurable:

ModeWho commitsHow
interval (default)this server--git-checkpoint-interval-secs <n>, default 60
nudge-onlyan external sync agent--git-checkpoint-mode nudge-only
disablednobody--git-checkpoint-interval-secs 0

Reach for nudge-only when an agent already owns the repository’s history. Per-minute checkpoint commits would fight it: two git actors in one working tree, and the agent’s coalesced, unit-of-thought commits buried under machine noise. In this mode the sweep still runs and still takes the lock, but it commits nothing — on a dirty tree it touches the sync-nudge sentinel and leaves the change exactly where it found it, unstaged, for the agent to pick up.

cdno-mcp-server --vault /srv/vault --git-checkpoint-mode nudge-only

The sentinel path is the one --sync-nudge-path sets, so the sweep and the per-write nudge always agree on it. You do not need --sync-nudge as well: that flag governs the per-write signal, and the two are useful together (writes nudge immediately; the sweep catches anything that arrived out of band) but independent.

Both non-default modes hand the trail to somebody else, and the server says so at startup — nudge-only logs a warning naming the sentinel and stating that this process commits nothing, and 0 warns that nothing in the process commits at all. If no agent is running, either is equivalent to having no recovery trail.

Only cdno-mcp-server sweeps. The stdio binary has no checkpoint loop and none of these flags.

When the sweep stops

The sweep’s job is to be the thing you can fall back on, so it must never fail quietly. Two ways it can stop, both loud:

  • A tick that runs and finds trouble — a non-zero git, the vault write lock busy, someone else’s paused merge or rebase — logs and retries on the next tick. Only a git that cannot be executed at all, five times running, disables the loop, with an error saying so.

  • A tick that never runs at all. The sweep does its work on a pool of worker threads, and if the process cannot get one, the tick is queued and the loop waits. Nothing is committed and, until this was fixed, nothing was logged either — the endpoint stayed up and tool calls kept answering, so nothing outside could tell. A tick that overruns three sweep intervals (never less than 30 seconds) now logs:

    ERROR git checkpoint sweep STALLED: a tick has not completed, no further tick can start, and so
    NOTHING in this process is committing — writes are no longer being recorded. ...
    

    It repeats while the tick stays stuck, and logs a recovery line if it completes. Alert on it: the two realistic causes are a process out of threads (see Thread budget) and a wedged git invocation, and the process cannot tell them apart from inside — but either way the vault has stopped being recorded.

Thread budget and pids_limit

A container sized with pids_limit counts every OS thread this process holds, not just processes. The server bounds itself so that number is knowable, and logs it at startup:

INFO runtime thread budget workers=8 max_blocking_threads=16 max_os_threads=25

max_os_threads is the ceiling: one main thread, one async worker per CPU the process can see, and a fixed pool of 16 for the blocking work (vault reads and writes, the reconciliation pass, the checkpoint sweep). Size pids_limit above that number with room for whatever else shares the container — a healthcheck that shells out needs to fork too, and a container that cannot fork reports unhealthy and refuses docker exec while the server itself carries on serving.

Note that workers follows the CPUs the process can see, which on Linux is CPU affinity, not a cgroup CPU quota: a small container on a large host still gets a worker per host core. Read the number off the log line rather than assuming it.

Pairing with a sync agent

A common shape is two clones of the vault repository — an always-on host running this server, and a laptop — with an external agent on the host owning the commit-and-push loop. That agent normally polls: it wakes on its own timer, sees a dirty tree, and commits. A write that landed a second after the last poll waits out the whole interval.

--sync-nudge closes that gap. After every write the server has verified, it rewrites a sentinel file, changing both its modification time and its contents. The agent watches that one path — launchd WatchPaths, inotifywait, fswatch, whatever it already uses — and acts immediately.

cdno-mcp-server --vault /srv/vault --sync-nudge
# → touches /srv/vault/.git/cdno-sync.nudge after each verified write

The contract is deliberately narrow:

  • Off by default. A deployment with no agent has nobody to signal.
  • One-way. The server writes the sentinel and never reads it, so an agent that is absent, stopped, or slow costs nothing but latency.
  • Only after a verified write. A write that failed, or that could not be read back, leaves the sentinel alone — an agent woken by writes that did not happen learns to ignore the signal.
  • Never fatal. A sentinel that cannot be written is logged and skipped; the write still succeeds.
  • Never content. The file holds a unix timestamp and nothing else. It names no note.

It lives under .git/ on purpose: git will not track it, and no tool that mirrors the working tree will carry it, so the signal cannot leak into the vault or across machines. --sync-nudge-path moves it if your agent needs it elsewhere; parent directories are never created.

Only cdno-mcp-server has this. The stdio binary is a local session with no agent on the other side of it, and offers no such flag.

Index freshness

Unlike a stdio session, this process is long-running while other writers — the CLI, editors, sync tools — mutate the Markdown underneath it. Markdown is the source of truth and the index is a cache, so the server re-runs the reconciliation pass on the configured interval as the correctness backstop. Out-of-band edits become visible to search_notes and the context tools within one interval at most.

Timezone — set TZ on the host

The server timestamps everything it writes — log lines, daily entries, tracking entries — with the process’s local time (chrono::Local::now()). A container or host with no zoneinfo database and no TZ set makes chrono fall back silently to UTC, so remote writes land hours behind the wall clock with no error. Any deployment must therefore ship a zoneinfo DB (tzdata on Alpine, already present on most distros) and set TZ, e.g. TZ=Europe/Stockholm.

At startup the server logs the offset it resolved, next to the “vault opened” line:

INFO local time zone resolved (server timestamps use process-local time) local_offset=+02:00 sample_local_now=2026-07-06T14:30:00+02:00

Check this line after deploying: a local_offset=+00:00 you didn’t intend is the tell-tale of a missing tzdata/TZ. (A host legitimately in UTC is fine — the line is factual, not a warning.)

Transport details

  • Endpoint: POST /mcp. Clients must send Accept: application/json, text/event-stream (the Streamable HTTP spec requires both).
  • Stateless JSON mode: every request is self-contained; responses are plain application/json (no SSE streams, no session ids). GET/DELETE on /mcp return 405.
  • Guardrails: request bodies are capped at 1 MiB and in-flight requests are bounded; the Host header is validated against the allowlist (403 otherwise).

Context-gathering tools

Read-only tools an assistant uses to understand your vault before acting. None of these mutate anything. Inputs marked optional may be omitted.

ToolInputsReturns
get_orientationenergy? (deep|medium|light)Commitments due soon (milestones and action dates of active and parked projects, standalone commitments, stewardship lines), active projects, lapsed stewardship habits, and a suggested starting point. Also focus (the current_focus shape, or null) and, on each project, last_paused (its most recent unresolved pause, or null) beside top_action, plus held_by, counts, waiting_on_empty and core_question as in the review fields, and slots (as on list_projects). Parked projects are not listed; suggest the day’s work from the projects that count. The MCP form of cdno orient, which renders neither focus nor last_paused.
get_project_contextproject (slug)A project’s state, next actions, milestones, waiting-on items, and links. Resolves active, parked and closed (_done/<year>/) projects. Also last_paused, as on get_orientation, so the re-entry hint is there when the project is opened and not only in the morning. The frontmatter carries held_by, and the payload counts.
get_portfolio_contentsportfolio (slug)Portfolio metadata plus its evidence inventory.
get_weekly_contextdate? (any day in the week)The weekly note’s sections (Wins, Challenges, One Improvement, This Week’s Goal), plus the week’s logs, completed_actions, the projects completed or dropped that week (closed_projects) and project state changes. Also projects: every active project, handed-off ones included, with the review fields.
get_monthly_contextdate?Monthly context for a strategic scan, including the past 30 days’ completed_actions as wins patterns, the projects completed or dropped in those 30 days (closed_projects), and every parked project (parked_projects). Parked entries and the stuck projects (active, unchanged for two weeks, handed-off ones included) carry held_by, counts and waiting_on_empty, as in the review fields. Also slots.
get_stewardship_trackingstewardship, activity, period? (e.g. 30d, 6m)Tracking entries for a stewardship/activity over a window, plus the activity’s declared contract in spec (record key, group field, and each metric’s type, unit, aggregate, and derived expression when it is computed rather than recorded — write that metric’s operands, never a field of its own name) when the vault declares one under [tracking.<activity>]. spec is null for an undeclared activity. Also returns series: this activity’s numeric trends, one per (group, metric) it declares, each point already reduced by that metric’s own aggregate, scoped and windowed by the same activity and period as the entries.
get_active_questionsdomain? (research|life)Active question notes, optionally filtered by domain.
get_commitmentslookahead_weeks? (default 2)The aggregated commitments view; overdue always included. A project’s milestones and action due dates appear while it is open, active or parked, and leave once it is completed or dropped; a standalone commitment appears whatever its project’s status.
current_focus—The action most recently started or resumed, unless it has since been completed, dropped or paused (focus is one slot), or null. Returns project, action, started, date, carried and origin (below). Replayed from the ## Logs of today and the [focus] carry_over_days days before it (default 1), so a start made from the CLI counts too, as does one written by hand in the log’s full shape (- **HH:MM**: started [[slug]] — text — stamp and em dash U+2014 both required; see cdno now); a completion, a drop or a pause clears it, and a promote_action in between renames it to the new note, keeping the start time. The MCP form of cdno now.
list_projectsinclude_closed? (default false)Active and parked projects with their frontmatter (held_by among it) and counts, plus the slot budget slots; with include_closed: true, completed and dropped projects too, under closed.
list_note_types—Every note type — the built-ins plus any config-defined [note_types.*] custom type — with its folder, required/optional fields, typed [schemas.*] field specs, template, and supplied placeholders. Call before create_custom_note to discover a vault’s custom types.
read_daily_notedate? (default today)The daily log for a date.
read_weekly_notedate? (default this week)The weekly note for an ISO week.
read_monthly_notedate? (default this month)The monthly note for a calendar month.
read_notenote (path, slug, type:slug or journal date)Any note, whole: path, note_type, frontmatter, body (uncapped), content_hash, backlinks and headings. Takes the references cdno open takes, plus a path without its .md.
search_notesquery, note_type?, from?, to?, portfolio?, limit? (default 20)Ranked full-text hits. The MCP form of cdno search.
lint—Vault-wide problems: frontmatter, broken wikilinks, a link that resolves only through an alias (a former name; the message names the current one), an alias that another note’s file stem shadows or a folder note’s name contests, attachment pairing, a project whose status disagrees with its folder, a custom template that lacks a key its built-in has gained, and lines the canonical parsers silently skip — malformed stewardship-dashboard bullets and daily-log focus markers (started, paused, resumed, action promoted on) cdno now will not read back, plus the same check on a renamed or relinked line and a warning for a link in its was: value.
triage_inbox—Pending inbox captures awaiting triage.

Review fields

Each element of get_weekly_context.projects:

{"slug": "harbour-survey", "held_by": "the harbour office", "counts": false,
 "waiting_on_empty": false, "core_question": null, "changed_this_week": true,
 "focus_days_while_held": 2}
FieldMeaning
held_byThe name of whoever else is moving the map, trimmed, only when held_by: names someone else. null when the map is the person’s own (no held_by:, or a value on [projects] holder_sentinels) or the value is unreadable (not a plain string).
countstrue when the map holds one of the max_active_projects slots: active, and naming no one else as its holder. Branch on this, never on the name.
waiting_on_emptytrue when ## Waiting On is missing, empty, or only (nothing yet). A holder over an empty Waiting On is a parked map wearing a name.
core_questionThe map’s core_question: as written, or null.
changed_this_weektrue when the map file was modified after the end of the Sunday before this ISO week’s Monday. A tool that touches the file without changing it also sets it, so it prompts a question, never a conclusion.
focus_days_while_heldFor a handed-off map (counts false), the number of distinct days from Monday to today with a focus started or resumed on it; 0 for any other map. Days, not starts. It is evidence for the person to weigh: no log line records who did the work.

The weekly review asks the person’s own projects whether they moved, and a handed-off project that changed this week what came back and what awaits the person’s decision. get_monthly_context’s parked and stuck entries carry held_by, counts and waiting_on_empty with the same meanings, so the monthly review tells a project shelved for someday from one someone else has.

Slots and counts

slots, on list_projects, get_orientation and get_monthly_context:

{"active": 3, "counted": 2, "cap": 5}
FieldMeaning
activeThe open projects with status: active, handed off or not: every active project shown.
countedThe active projects that hold a slot, naming no one else in held_by:. The cap is checked against this number.
capThe configured max_active_projects.

Each list_projects row (active, parked and closed) and the get_project_context payload carry counts, with the meaning in the review fields, and their frontmatter carries held_by exactly as written, since it is the frontmatter view: a sentinel comes through as its text there, while the summaries’ held_by reads null for it. Branch on counts, never on the holder’s name. list_projects stays a listing that reads no map body, so its rows carry no waiting_on_empty; the orientation projects carry it.

Focus fields

current_focus, the focus field of get_orientation and the focus field every focus-carrying write result share one shape:

{"project": "surrogate-model", "action": "Draft the methods section (deep)",
 "started": "08:50", "date": "2026-10-04", "carried": false,
 "origin": {"started_at": "2026-10-03T14:05"}}
FieldMeaning
projectProject slug.
actionThe bullet text exactly as logged, energy suffix and all: the string complete_action expects back. After a promote_action it is the new note’s link, [[actions/<slug>]] (energy).
startedHH:MM of the open marker (the started, or the resumed that re-anchored it).
dateThe day of that marker, YYYY-MM-DD.
carriedtrue when date is not today: the focus was left open on an earlier day.
origin{started_at} of the earlier start a resume continued, else null. A resume after a pause has none.

null means nothing is open, wherever it appears.

last_paused, on get_orientation’s projects and on get_project_context:

{"project": "surrogate-model", "action": "Draft the methods section (deep)",
 "title": "Draft the methods section", "at": "2026-10-04T11:13", "date": "2026-10-04",
 "next": "pick up at \"Prior approaches\"", "reason": "lunch"}

It is the project’s most recent pause that no later started, resumed, completion, drop or promotion of the same action followed, looked for in the last paused_lookback_days days; next and reason are null when the pause had none. Quote next back to the person when they pick the project up. The server instructions tell an agent how to use the focus; see Write tools.

Notes

  • Dates are YYYY-MM-DD. Week-scoped tools accept any day within the target ISO week; month-scoped tools accept any day within the target calendar month.
  • search_notes returns the same hit shape as the CLI: path, note_type, title, snippet, score. See JSON output.
  • completed_actions (weekly and monthly) covers both forms an action takes. One with its own note carries slug and path alongside source: "note"; an inline bullet — the default form, and so the common case — carries source: "bullet" with both null. A client must expect null there. A completion that has both a note and a log line is listed once, and dropped actions never appear. The list is read from the daily notes directly rather than from the capped logs field, so a completion early in a busy week is not lost to that cap.
  • read_note reads any note, of any type. content_hash is the hash of the bytes just read, the value to hand back as expected_hash when revising the note, so a change made in between is detected rather than overwritten. backlinks comes from the index, which every cuaderno write updates; a link made by editing a file elsewhere appears once the index next reconciles. A slug several notes share is refused with code ambiguous_slug and the candidates’ paths in details.candidates; a reference matching nothing is refused with code not_found. headings lists every heading in the body with inline markup stripped, the form revise_note’s section takes. To follow a heading link such as a concept’s origin ([[journal/2026/daily/2026-09-02#Woodbury identity]]), pass the part before the # and find the entry under that heading: the whole link, anchor included, is refused as not_found. When note was a former name of a renamed note, the payload adds found_by_former_name (the name typed) and path shows where the note is now: use the current slug from then on.
  • These pair naturally with the write tools: read context, propose an action, then write it.

Write tools

Tools that mutate the vault. Each returns a result describing what was written. The same business rules as the CLI apply (append-only notes, auto-logged project state, the project cap).

Every write is verified

A tool result that only said “success” could not be told apart from a write that silently never landed — which over a remote connection is exactly how a lost note goes unnoticed. So every write tool re-reads its target before answering, and the result carries the evidence:

{
  "path": "journal/2026/daily/2026-08-26.md",
  "message": "Logged to journal/2026/daily/2026-08-26.md",
  "verification": {
    "verified": "content",
    "bytes_written": 412,
    "content_hash": "84bc0919e867576f",
    "appended_tail": "- **09:14**: baseline sweep finished\n"
  }
}
FieldMeaning
verifiedcontent — the file was re-read; or removed, for discard_inbox_item, where the check is that the file is gone
bytes_writtenSize of the file on disk after the write. 0 for a removal
content_hashThe note’s content hash (below) — null for a removal
appended_tailFor append-shaped writes (append_to_log, start_action, note_to_daily), the tail of the section the text went into — see below. null elsewhere

If the target cannot be read back, the tool returns an error rather than a success. The wording says the write is unverified, not failed: it may still have landed, so the right response is to re-read the note, not to blindly repeat the write.

The appended tail

appended_tail is scoped to the section the write targeted, not to the last bytes of the file. append_to_log and start_action both write into ## Logs, so that is the section you get back. note_to_daily writes into both ## Notes and ## Logs; its tail is the last 512 bytes of ## Notes, which end with the entry just written (the pointer line is already in the result’s log_line). (start_unplanned_action also logs, but it rewrites the project map in the same commit and so verifies as a whole-file rewrite — its appended_tail is null.)

The distinction matters because ## Logs is not always last. Cuaderno pins the effective daily template’s last ## section to the bottom of the note — for the built-in template that is ## Logs, but a custom .cuaderno/templates/daily.md ending in, say, ## Reflection keeps Reflection last and leaves the log line in the middle of the file. Reading the end of the file would then show you Reflection’s text while claiming to be the line that landed.

If the section cannot be located — an unparseable note, a ## Logs heading that is missing or duplicated — the field is null. The write is still verified by bytes_written and content_hash; only the extra evidence is withheld, because a window over the wrong bytes is worse than no window.

The content hash

content_hash is the same non-cryptographic xxh3-64 fingerprint (16 lowercase hex characters) the index uses for change detection, so a client can recompute it over a note it has read and compare. Two uses in practice:

  • confirm a note is byte-identical to the one the server saw;
  • notice a no-op. update_project_state with a state that already matches deliberately writes nothing and still reports success — an unchanged content_hash across two calls is how you tell.

It is a change detector, not tamper evidence: it does not defend against someone who also chooses the content.

Logging, capture, triage

ToolInputsEffect
append_to_logtextAppend a line to today’s daily note. (cdno log)
capturetextDrop a raw note into inbox/. (cdno capture)
discard_inbox_itemslugClear a triaged capture (slug from triage_inbox).

When a focus is open, append_to_log, capture, note_to_daily and discard_inbox_item tag what they write with it; see The during: tag.

Evidence

ToolInputsEffect
file_to_portfolioportfolio, source, origin, content?, attach?, vars?File evidence into a portfolio; attach is a server-side path to a non-Markdown artefact (vars is ignored on the attach path). (cdno file)

Projects, actions, milestones, waiting-on

Every tool in this table takes an open project, active or parked: a parked map is a complete map, so add_action, update_project_state, the milestone and waiting-on tools, set_core_question, promote_action, complete_action, drop_action and hand_off_project write to it as it is. Never activate a parked project just to write on it; the cap is checked only when a project begins to count (activate_project, or a take-back of an active project). Only starting or picking up work (start_action, start_unplanned_action, switch_action, switch_unplanned_action, resume_action) needs an active map, as does park_project; on a parked one they return project_not_active with status: "parked" (see the rejection codes). A completed or dropped project takes none of these writes.

ToolInputsEffect
update_project_stateproject, new_stateRewrite the Current State (auto-logs the previous).
set_core_questionproject, core_question?, clear?Set the project’s core question (bare questions/<domain>/<slug> target, not [[…]]); clear: true detaches. Auto-logs the previous value.
hand_off_projectproject, holder?, take_back?Record whose move an open (active or parked) project is: holder writes held_by: "<name>" and the project stops counting against the cap; take_back: true writes held_by: null. Exactly one of the two. Logs holder on [[slug]] with was:/now: lines. Refused with empty_field for a name on the vault’s [projects] holder_sentinels, with project_in_focus while an action on the project is in focus, and, for a take-back that would make an active project count at the cap, with project_cap_reached. Taking back a parked project only clears the name.
add_actionproject, title, energy, with_note?, slug?, vars?Append a next action; with_note also scaffolds a manifest note (slug and vars apply only then; without with_note, slug is ignored).
promote_actionproject, query, slug?, vars?Promote a bullet to a manifest note (substring match); slug names the new note’s file (the slug rule).
start_actionproject, queryLog that work on an existing bullet is starting. Logs the resolved bullet text, so the later close pairs with it. Errors when query matches nothing — it will not create the action. Refused with focus_open while another action is in focus (below).
start_unplanned_actionproject, title, energyAdd the bullet and start it, in one commit, for work that was on no map. Separate from start_action on purpose: a fallback would turn a typo into a new action.
pause_action, switch_action, switch_unplanned_action, resume_actionsee Focus toolsMove the one focus slot: set it aside, move to another action, or pick work up again.
complete_actionproject, queryComplete an action; archives its note if any.
drop_actionproject, query, reason?Close an action without recording it as done (superseded, abandoned, reprioritised); archives its note as status: dropped.
add_milestoneproject, title, target_date?, hard?Add a milestone; hard counts it in commitments and requires target_date. Omit target_date for a condition-gated milestone (target: TBD), which stays out of commitments.
complete_milestoneproject, queryComplete a milestone (substring match).
drop_milestoneproject, query, reason?Remove a milestone, with the lines indented beneath it, without recording it as met (superseded, mis-typed, not happening); logs milestone dropped on. Completed bullets are never matched.
add_waiting_onproject, descriptionAdd a waiting-on blocker.
resolve_waiting_onproject, queryResolve a waiting-on item (substring match).

Focus tools

Four tools move the focus, the one “what I am on” slot that current_focus reads back. They are the MCP forms of cdno action pause, switch and resume. Each writes one entry in today’s ## Logs and, like every write, is verified.

ToolInputsEffect
pause_actionnext?, reason?Sets the current focus aside without finishing it. Takes no project or query: it pauses whatever current_focus shows, with nothing matched against the map. Logs action paused on [[slug]] — <text> with optional indented next: and reason: lines. Result adds paused (the focus that was paused).
switch_actionproject, query, next?, reason?Pauses the current focus and starts an action already on the map, in one commit. project and query name the action to switch to; next and reason describe the one set aside. With nothing in focus it is a plain start and paused is null.
switch_unplanned_actionproject, title, energy, next?, reason?The same, for work on no map yet: adds the bullet and starts it. Separate from switch_action for the reason start_unplanned_action is separate from start_action: a query that matches nothing must never become a new action.
resume_actionproject?Re-anchors a focus carried over from an earlier day, or reopens the latest pause within paused_lookback_days; with project, that project’s carried focus, else its latest pause. Logs resumed [[slug]] — <text>.

switch_action and switch_unplanned_action return paused (the focus that was set aside, or null) and started (the new focus). resume_action returns resumed (the focus now open) and resumed_from:

{"resumed": {"project": "surrogate-model", "action": "Draft the methods section (deep)",
             "started": "11:12", "date": "2026-10-04", "carried": false, "origin": null},
 "resumed_from": {"kind": "paused", "date": "2026-10-04",
                  "next": "pick up at Prior approaches", "reason": "call"}}

resumed_from.kind is carried or paused, next and reason are the pause’s (always null for a carried focus). With project, a carried focus wins over that project’s pause, so read kind and resumed.action back to the person rather than assuming the pause they were shown, and read next out as the re-entry cue. pause_action and resume_action are for the person’s word: the tool descriptions tell an agent never to call them unasked.

Rejections: focus_open and no_focus

Two refusals are returned as coded rejections, so an agent can act on them without parsing prose. Nothing is written.

focus_open comes from start_action, start_unplanned_action, switch_action, switch_unplanned_action and resume_action:

{"code": "focus_open",
 "message": "An action is already in focus. Ask the person before switching; do not retry.",
 "details": {"focus": {"project": "surrogate-model", "action": "Draft the methods section (deep)",
                       "started": "09:10", "date": "2026-10-04", "carried": false},
             "attempted": {"project": "other-project", "query": "CI"},
             "same_action": false,
             "remedy": "switch_action"}}
details keyMeaning
focusWhat is open: project, action, started, date, carried.
attemptedWhat the call asked for: {project, query}, or {project, title} for an unplanned form. Absent from resume_action’s rejection, which attempts no target.
same_actiontrue when the call named the action that is already in focus.
remedyThe tool to call, on the person’s word: switch_action (something else is in focus), resume_action (the same action, carried over from an earlier day), or already_focused (the same action, started today: nothing to do).

The message tells the agent to ask the person before switching and not to retry. The server never switches on its own.

no_focus comes from pause_action (Nothing is in focus to pause.) and resume_action (Nothing to resume: no focus carried over and no pause.), with empty details. Tell the person there is nothing to act on rather than retrying.

focus on write results

The success payload of these eighteen tools carries a focus field: start_action, start_unplanned_action, switch_action, switch_unplanned_action, pause_action, resume_action, complete_action, drop_action, promote_action, add_action, append_to_log, capture, note_to_daily, park_project, activate_project, complete_project, drop_project and hand_off_project. It is the focus as current_focus reads it right after the write, so an agent sees what is open without a second call: the same shape, and null means nothing is open. It is read in the same call that verifies the write, and a focus that cannot be read after a write has landed comes back as null rather than as an error, because an error would tell the agent that a successful write failed. Every other write tool’s result is unchanged.

The server never refuses a write for being outside the focus. How an agent should behave when a request is, is stated once in the server’s instructions rather than in each tool description: compare at project level, say one sentence only on a mismatch, offer to capture it, and treat the person’s explicit “let’s switch to Y” as consent. See Focus.

The during: tag

When a focus is open at write time, a carried one included, the writes that park a thought say which project it was written during:

  • append_to_log and note_to_daily append an indented continuation to the log line:

    - **11:12**: checked the mesh
      during: [[surrogate-model]]
    

    (note_to_daily also echoes the tag on a second line of the log_line it returns.)

  • capture adds captured_during: surrogate-model to the new inbox item’s frontmatter.

  • discard_inbox_item copies that tag from the item onto the log line it writes, so it outlives the file.

With no focus open nothing is written differently, and a focus that cannot be read writes untagged rather than failing. The tag is written by the server; an agent does not add it to its own text.

Commitments and tracking

ToolInputsEffect
create_commitmenttitle, due, context, project?, stewardship?, slug?, vars?Create a standalone commitment note; slug names its file (the slug rule).
complete_commitmentcommitment (slug)Mark a commitment done and archive it.
drop_commitmentcommitment, reason?End a commitment that was not kept; stamps status: dropped, clears completed, archives it. Never appears as completed work.
reschedule_commitmentcommitment, dueMove an active commitment’s due date, logging both the old and new dates. Refuses an unchanged date.
complete_periodicstewardship, title, at?Complete one occurrence of a stewardship’s periodic commitment, rolling next: forward by that line’s recurrence. Anchored to the due date, so completing early never drags the schedule earlier.
create_tracking_entrystewardship, activity, routine?, content?, vars?, metrics?, date?File a tracking note under an expanded stewardship. metrics is a JSON object merged into the entry’s frontmatter — a scalar per reading ({"balance": 1240.5}), or an array of flat records when one entry holds several comparable items ({"detail": [{"subject": "harmony", "minutes": 25}]}); a scalar whose key is declared under [schemas.tracking.fields] is type-checked, and a key naming the note’s identity (type, stewardship, activity, date) is refused. date files the entry for a past day (bounded to 50 years back, 1 year ahead). A second call for the same (activity, date) merges into the first: content appended, metrics folded in. Records carrying a stable id replace the record with that id; records without one append, so re-sending a payload without ids double-counts summed metrics. Either way the write is journalled to today’s daily log.

Revising a note

ToolInputsEffect
revise_notenote, reason, and either body + expected_hash or section + contentRefine a mutable custom note (a concept, say) in place, logging the revision to today’s daily note in the same write. (cdno note revise)

revise_note takes exactly one of two forms:

InputMeaning
noteWhich note: any reference read_note takes — a vault path with or without .md, a bare slug, or type:slug.
bodyWhole-body form. The note’s new body, everything after the frontmatter (which is kept as it is), written verbatim.
expected_hashRequired with body: the content_hash read_note returned. A blank value counts as missing. Ignored with section.
sectionSection form. Heading text of the section to upsert, as read_note’s headings lists it, without the # markers. An existing section is replaced together with its sub-sections; a missing one is appended as ## <section>. No hash is needed.
contentThe section’s new text, without its heading. Only with section.
reasonRequired. Why the note was revised, in a short clause; the assistant drafts it from what it changed. It becomes the daily-log line revised [[<path>#<section>]] — <reason>, or revised [[<path>]] — <reason> for a whole body.

Text identical to the note’s current text writes and logs nothing and returns changed: false. The result extends the usual write result with changed, new_hash (the note’s hash after the call: pass it as expected_hash on a follow-up revision rather than reading again), log_line (the line written, without its timestamp, or null) and section_target (the anchored link the log line points at, for a section revision); verification is null when nothing changed.

Refusals (a rejection with these codes; nothing is written):

CodeWhen
note_not_revisableThe note is of a built-in type (its sections belong to its own tools) or of a custom type declared append_only = true.
stale_revisionA whole-body revision whose expected_hash no longer matches the bytes on disk: the note changed since it was read. details.actual is the current hash; read the note again and redo the edit on the fresh text.
revision_invalidThe section heading is empty, spans lines, contains [, ], | or #, or starts with ^; or content holds a heading at the section’s own level or above, which would restructure the note.
ambiguous_sectionThe section heading matches more than one heading in the note.
not_found / ambiguous_slugThe note reference matches no note, or several (with their paths in details.candidates), as for read_note.

A blank reason, body together with section, section without content, content without section, or body without expected_hash is refused as an invalid argument (-32602) naming the field.

Renaming a note

ToolInputsEffect
rename_notenote, to or relink: true, reason?, dry_run?, expected_plan?, skip_sync_check?Rename a note’s slug and follow every reference to it in one transaction, logging a renamed line to today’s daily note; or, with relink, keep the slug and rewrite the links still under a former name. (cdno note rename)

The preview is the contract. The sequence an agent follows:

  1. Preview. Call with dry_run: true. The result is the plan and nothing is written (written: false); a refusal here is exactly the refusal a write would get.
  2. Show. Put the plan in front of the person: what moves, how many references in how many notes, the log_line, any warnings. The person decides, not the agent.
  3. Write. Call again with the same note, to and reason (the reason is part of the plan) and expected_plan set to the preview’s plan_hash. What is written is the plan that was shown; if the vault changed in between the call is refused with stale_plan (below) and writes nothing.

note takes the references read_note takes, including a former name of the note (found_by_former_name in the result says which); reason is a short clause for the log line. A rename to the note’s current slug is an empty plan: written: false, log_line: null.

Relinking. relink: true in place of to keeps the note’s slug and rewrites every link that reaches it only through a former name, links and reference fields alike (the links are the stragglers lint reports, which a sync merge can bring back), to the current slug. The same preview-then-write sequence applies, with relink: true on both calls. Nothing moves and the aliases stay listed: the result has moves: [] and new_slug equal to from, and the log_line is relinked [[<slug>]] — <title> with an indented references: <n> line. With no straggler the plan is empty and nothing is logged. An archived action note can be relinked, though not renamed. A blank to beside relink: true reads as omitted. relink with to, either with reason, or neither to nor relink, is refused as an invalid argument naming the field (cdno note rename --relink).

The result is the plan, whether or not it was written: dry_run, target, from (the note’s current slug), new_slug, found_by_former_name (the former name note matched, or null), moves ({from, to}), edits (a path with its changes, each of kind links with a count, field with a key, alias or log_line), references, notes, warnings (each with a kind and a message: frozen_prefix_already_diverged, old_slug_already_an_alias, or sync_check), plan_hash, log_line (null when the plan is empty), then written, index_stale (true: the files are written and the rename succeeded; the index catches up on the next open) and verification. A write nudges the sync agent only when the HTTP server runs with --sync-nudge (off by default; the stdio server never does); a preview never does.

Before planning, the tool checks the vault against its upstream as last fetched: it never fetches. A preview is refused exactly as a write would be.

Refusals (a rejection; nothing is written):

CodeWhen
stale_planThe vault changed since the preview (any log append does it, and so does a different reason). details carries expected and actual. Preview again and show the person the new plan; never retry the same expected_plan. A blank expected_plan is refused as an invalid argument.
behind_upstreamThe vault is details.behind commits behind its upstream. Retry in a few seconds; if still behind, the sync is stalled: tell the person rather than set skip_sync_check.
git_operation_in_progressA merge, rebase or similar was left paused. Never overridable. Both git codes carry the check’s warnings in details when it had any.
stem_sharedAnother note shares the slug, so links to it cannot be told apart.
note_not_renamableThe note is of a kind the rename does not take (details.reason: archived, folder, a type). A portfolio whose slug a question shares is renamed through the question.
reference_drift / reference_unaddressableA referrer’s frontmatter would change beyond the reference, or names the note in a form the rewrite cannot address; the person fixes the note by hand first.
rename_incompleteA write failed and the rollback could not restore details.paths: the person checks them by hand.
slug_taken / invalid_slugto is taken or malformed: refused, never suffixed.
not_found / ambiguous_slugThe note reference matches no note, or several, as for read_note; a question and its same-slug portfolio resolve to the question.

Frontmatter

ToolInputsEffect
set_frontmatternote, key, valueSet a declared, settable = true typed frontmatter field through the index (no desync). note is today, a YYYY-MM-DD date, or a vault-relative path. Engine-owned keys (type, status, a period key, and a project’s closed, owned by complete_project / drop_project / activate_project) are rejected; the value is type-checked; log_on_change fields stamp a daily-log line. (cdno frontmatter set)

Daily, weekly, and monthly sections

ToolInputsEffect
note_to_dailyheading, body, date?Write worked-out substance to a daily note as one ### <heading> entry under ## Notes, with its pointer line in ## Logs, in one write. See below. (cdno log note)
upsert_daily_sectionsection (Standup|Intention|Agenda|Meeting|Notes), content?, date?, append?Write or append a daily-note section. Notes is append-only: it takes append: true only, and append: false is refused; for one entry with its ## Logs pointer, use note_to_daily.
upsert_weekly_sectionsection (Wins|Challenges|One Improvement|This Week's Goal), content?, date?, append?Write or append a weekly-note section.
upsert_monthly_sectionsection (Wins|Themes|Next Month's Focus), content?, date?, append?Write or append a monthly-note section.

note_to_daily

InputMeaning
headingThe entry’s heading, written as ### <heading> under ## Notes and used verbatim as the pointer’s anchor.
bodyThe entry’s substance. Leading blank lines and trailing whitespace are dropped. Its wikilinks are copied onto the pointer line.
dateISO YYYY-MM-DD of the daily note to write to, stamped at the current time. Omitted = today.

The entry is appended under the day’s ## Notes, which is created immediately above ## Logs if the day has none (the daily note itself is created if absent). The pointer line noted [[journal/<year>/daily/<date>#<heading>]], followed by the body’s wikilinks in parentheses when it has any, is appended to ## Logs in the same write, so the entry is not logged again with append_to_log. End an entry worth reusing with the tag #concept on its last line, so a later review can find it as a candidate for promotion to a concept note.

The result adds target — the entry’s anchored link, journal/<year>/daily/<date>#<heading>, to cite as [[<target>]] (for example from a concept’s origin) — and log_line, the pointer line without its timestamp.

Refused with code history_entry_heading_invalid (nothing is written) when the heading reuses a daily section name (Standup, Intention, Agenda, Meeting, Notes, Logs), matches a heading already in that day’s note, contains [, ], |, # or inline markup (bold, italics, code), or starts with ^. Headings inside the body must be level 3 or deeper and are held to the same uniqueness rule, so a pasted derivation with its own ## Proof must be demoted first. A blank heading or body is refused as an invalid argument (-32602).

Notes

  • append? defaults to replacing the section; set it true to append instead.
  • Dates are YYYY-MM-DD; week-scoped tools accept any day in the target week, and month-scoped tools accept any day in the target month.
  • vars? is an optional name -> value map supplying values for a custom template’s [variables.prompt] placeholders — the MCP analogue of the CLI’s repeatable --var name=value. Omitting a required prompted variable fails with an “unresolved prompts” error. See Creation and lifecycle tools for the full list of templated tools that accept it.
  • See also: Creation and lifecycle tools, JSON output.

Creation and lifecycle tools

Tools that create new notes or move existing ones through their lifecycle.

Creation

ToolInputsEffect
create_projecttitle, context, core_question?, slug?, vars?Create a project (parked if at the active cap). (cdno project create)
create_portfolioquestion, project?, slug?, vars?Create an evidence portfolio. question is free text or an existing question’s slug; given a slug, the portfolio takes that question’s text as its title, links both ways, and slug must be omitted or equal to it (portfolio_slug_mismatch otherwise).
create_questiondomain (research|life), text, slug?, vars?Create a question note.
create_stewardshipname, context, expanded?, slug?, vars?Create a stewardship; expanded adds a tracking/ folder.
create_custom_notetype_name, title, fields?, slug?, vars?, body?, origin?Create a note of a config-defined custom type ([note_types.<name>]); built-in types have their own dedicated create tools, on this page and under Write tools. fields is a name → value map of the type’s declared frontmatter fields — every required one must be present, and each key must be declared. body excludes the title heading (the engine writes the H1) and fills the template’s {{body}} placeholder, or is inserted after the H1; origin is one string of wikilinks to where the note came from, always stored as a frontmatter string, and is refused unless the type declares an origin field. Creation is logged to today’s daily note as <type> created [[<folder>/<slug>]] — <title>; a promotion (creation with origin) needs no other log line. Pass slug to name the file; passing it beside a slug entry in fields is refused (slug_given_twice). Without slug, a second note with the same title is not refused but created as <slug>-2, so for a concept, search the library (search_notes with note_type: concept) and refine an existing note with revise_note instead. Call list_note_types first to discover a vault’s types and their fields. (cdno note create)
link_portfolio_to_questionportfolio, questionRetrofit a portfolio→question link (backlinks both ways).
link_portfolio_to_projectportfolio, projectRetrofit a portfolio→project link (sets project: and appends to the project’s Links).

Lifecycle

ToolInputsEffect
park_projectprojectMove an active project to _parked/.
complete_projectprojectClose an active or parked project whose work is done: move it to _done/<year>/ with status: completed and closed:. Refused while any action or milestone is open. (cdno project complete)
drop_projectproject, reason?, open_items? (refuse|drop), expected_open_items?Close an active or parked project that is not going to happen: status: dropped, closed:, moved to _done/<year>/. Refused with the open items by default; open_items: "drop" with the refusal’s hash drops them too. (cdno project drop)
activate_projectprojectBring a parked or closed project back, clearing closed: (enforces the five-project cap).
set_question_statusquestion, status (active|parked|answered|retired)Transition a question’s status.
add_periodic_commitmentstewardship, title, recurrence, next_dateAppend a periodic commitment to a stewardship dashboard.

The holder

ToolInputsEffect
hand_off_projectproject, holder?, take_back?Record whose move an open (active or parked) project is. Exactly one of holder (the name the person gives, written as held_by: "<name>") or take_back: true (writes held_by: null); omitting both is an error, never a silent take-back. The map keeps its status and folder. Logs holder on [[<slug>]] with indented was: / now: lines, (mine) for no holder; naming the holder it already has changes nothing. (cdno project hand-off, take-back)

An active project that names a holder does not count toward the cap (counts: false, and slots.counted on list_projects leaves it out). Handing off is never the way to make room at the cap: call it only on the person’s word that someone else is moving the project. Rejections:

  • empty_field (remedy: take_back): an empty holder, or one on the vault’s [projects] holder_sentinels (a name meaning the person or no one in particular). If they cannot name who has it, it is theirs.
  • project_in_focus: an action on the project is in focus; the person pauses it first.
  • project_cap_reached: taking back an active project would make it count with the counted projects already at the cap; its details carry current, max and active_projects, which lists only the projects that count. Taking back a parked project only clears the name and never activates it.
  • project_not_active: the project is completed or dropped.

The tool also appears with the other project writes under Write tools.

Naming a note

The eight create tools (create_project, create_question, create_portfolio, create_stewardship, create_custom_note, add_action, promote_action, create_commitment) take an optional slug naming the new note’s file; omitted, it is derived from the title. Propose one when the title is long. It follows the slug rule: lowercase a-z, 0-9 and single -, at most 50 characters, not something YAML reads as a number, boolean, null or date. An explicit slug is used exactly or refused, never suffixed with -2. Refusals are coded rejections with nothing written: invalid_slug, slug_taken, portfolio_slug_mismatch and slug_given_twice, listed with their details under When a call is rejected. add_action ignores slug unless with_note is true: an inline bullet has no file.

Notes

  • context is one of the fixed life domains.
  • recurrence follows the recurrence syntax: daily, weekly, monthly, yearly, or every N months.
  • activate_project enforces the cap — if activating would make more projects count than the cap (active and naming no holder), the call fails and the assistant must park one first.
  • Closing a project never needs a slot, and works on a parked project directly. While an action or milestone is open, both closing tools return a project_has_open_items rejection whose details list the actions, the milestones (with date and hard), the untouched_commitments (linked standalone commitments, which no close ever touches) and an open_items_hash. complete_project stays refused until each item is completed or dropped: a completion is a claim that the work was done. drop_project with open_items: "drop" and expected_open_items set to that hash drops every listed item with the project, each logged with reason: project dropped (<reason>) (plain reason: project dropped when no reason was given); if the list has changed since, it is refused again with the new list and hash. The result names dropped_actions, dropped_milestones and untouched_commitments.
  • A closed project can take the other outcome later (complete_project on a dropped one, and the reverse), re-filed under this year’s folder with a new closed: date. The same outcome again is a project_not_active rejection whose details carry status and closed.
  • vars? is an optional name -> value map supplying values for a custom template’s [variables.prompt] placeholders — the MCP analogue of the CLI’s repeatable --var name=value. Supply an entry for each prompted variable the note’s template uses that has no static [variables] default; otherwise creation fails with an “unresolved prompts” error (MCP has no interactive prompt to fall back on).
  • See also: Write tools, Context-gathering tools.

Using with Claude skills

The MCP tools are building blocks. A skill composes them into a repeatable ritual the assistant can run on request — “do my daily orientation,” “walk me through the weekly review.” Cuaderno ships worked examples you can adapt.

Where the examples live

The repo’s examples/skills/ directory contains three reference skills:

  • quick-capture — capture a thought into the inbox with minimal friction.
  • daily-orientation — read your orientation and help set the day’s intention.
  • weekly-review — celebrate the week’s wins first, scan stewardship habits, walk the projects, look two weeks ahead, and pick one goal for next week, written to the weekly notes.

Each is a directory with a SKILL.md (YAML frontmatter + a Markdown body of steps), plus shared references/. The README there documents the authoring pattern.

The pattern

A skill typically:

  1. Surfaces context with a read tool — e.g. get_orientation or get_weekly_context.
  2. Talks it through with you, deciding what to do.
  3. Writes with the matching tool — e.g. append_to_log, upsert_daily_section, update_project_state.

For example, a daily orientation skill calls get_orientation, presents the commitments and the suggested start, asks for your intention, and writes it with upsert_daily_section(section="Intention", ...).

A weekly review skill calls get_weekly_context, whose projects carry the review fields, and asks each project the question its relationship implies rather than one question of all of them:

The projectThe question
Yours (counts: true)Did it move?
Handed off, and changed_this_weekWhat came back, and what is waiting on your decision?
Handed off over an empty Waiting On (waiting_on_empty)A name over an empty Waiting On is a parked map wearing a name: park it, or take it back?
Handed off, with focus_days_while_held of two or moreOffered as evidence: take it back, or change the name to whoever really has it?

The skill asks and never concludes: the fields are evidence, and the person decides. It never offers a hand-off as a way to make room at the cap. See Weekly review.

Graceful degradation

Because each step maps to a discrete tool, a skill can degrade gracefully — if a write tool isn’t available or you decline it, the read half still gives you the briefing. The example skills show how to bind steps to tools and reference shared material.

Build your own

Start from an example, swap in the tools your ritual needs (see reads, writes, creation & lifecycle), and keep each step mapped to one tool so the flow stays inspectable. Then install it like any other Claude skill.

See also

JSON output

Adding --json to a supported verb swaps the formatted table for machine-readable JSON. These shapes match the MCP server DTOs, with the exceptions noted below, so you get the same structures from the CLI and from an AI client. (Which verbs support --json is covered in the CLI overview.)

Write verbs → a result object

Every write verb emits the same small object and runs non-interactively (project complete and project drop add to it; see below):

{
  "path": "projects/surrogate-model.md",
  "message": "Created projects/surrogate-model.md"
}

path is the vault-relative file written or updated; message is the human-readable line. For the two-file cases (e.g. action add --note), path is the file the verb considers primary.

project complete / drop --json → a result or a refusal

A close that goes ahead emits the result object with three more arrays: what the drop let go, and the linked standalone commitments, which no close ever touches.

{
  "path": "projects/_done/2026/alpha-study.md",
  "message": "Dropped alpha-study. 0 of 5 slots in use. Let go: 1 action, 1 milestone.",
  "dropped_actions": ["Define first concrete step (light)"],
  "dropped_milestones": ["Submit"],
  "untouched_commitments": []
}

A close refused because actions or milestones are open prints the rejection object the MCP server returns, on stdout, and exits 1:

{
  "code": "project_has_open_items",
  "message": "project 'alpha-study' has 2 open item(s) — tick the ones that are done, or let them go explicitly",
  "details": {
    "slug": "alpha-study",
    "actions": [{ "text": "Define first concrete step (light)", "note": null, "note_status": null }],
    "milestones": [{ "title": "Submit", "date": "2026-12-01", "hard": false }],
    "untouched_commitments": [],
    "open_items_hash": "76d0efb83a540bf9"
  }
}

Slug refusals

An explicit --slug that is refused prints the rejection object on stdout and exits 1, on any of the eight create verbs (the slug rule). The shape is the one the MCP server returns, and details depends on the code:

codedetailsWhen
invalid_slugslug, whyThe slug breaks the shape rule; why says which part.
slug_takenslug, holder, kindAnother note already holds the name. holder is that note’s path; kind is stem (its filename), folder (the folder of a folder note) or alias (a former name another note still lists).
portfolio_slug_mismatchquestion_slug, slugportfolio create --question <question-slug> with an explicit --slug that is not that question’s slug.
slug_given_twicefieldnote create given --slug beside --field slug=…; field is slug.
cdno portfolio create --question "Sparse vs dense attention OOD" --slug surrogate-model --json
{
  "code": "slug_taken",
  "details": {
    "holder": "projects/surrogate-model.md",
    "kind": "stem",
    "slug": "surrogate-model"
  },
  "message": "slug 'surrogate-model' is taken by 'projects/surrogate-model.md' (as its stem)"
}

Nothing is written on a refusal. Do not retry with a -2 of your own; ask for another name.

Rename refusals

cdno note rename --json prints the plan on success (see note rename):

{
  "dry_run": false,
  "target": "projects/old.md",
  "from": "old",
  "new_slug": "surrogate-model",
  "found_by_former_name": null,
  "moves": [{ "from": "projects/old.md", "to": "projects/surrogate-model.md" }],
  "edits": [
    { "path": "journal/2026/daily/2026-11-06.md", "changes": [{ "kind": "links", "count": 2 }, { "kind": "log_line" }] },
    { "path": "projects/old.md", "changes": [{ "kind": "alias" }] }
  ],
  "references": 2,
  "notes": 1,
  "log_line": "renamed [[surrogate-model]] — Old\n  was: old",
  "warnings": [],
  "plan_hash": "5c380833154ebb71",
  "written": true,
  "index_stale": false
}
FieldMeaning
dry_runtrue for a preview.
target, from, new_slugThe note’s path and current slug before the rename, and the slug it takes.
found_by_former_nameThe former name the selector matched, or null.
moves{from, to} pairs; a portfolio’s folder, or a question and its portfolio, move together.
editsOne {path, changes} per rewritten path, in path order; see changes below.
references, notesReferences rewritten (links plus fields) and the notes holding them.
warnings{kind, message, …} entries that do not stop the plan; kinds below.
plan_hashAn opaque digest of the plan; the MCP tool takes it back as expected_plan.
log_lineThe renamed entry without its time stamp, or null for an empty plan.
written, index_staleWhether anything was written; whether the index failed to follow (files are correct, the next open reconciles).
verificationMCP only: the read-back proof, null unless a write landed.

written is whether anything was written (false on --dry-run and for a rename to the current slug); log_line is the unstamped renamed entry appended to today’s daily note. Under --relink the shape is the same, with moves empty, new_slug equal to from, written false when there was no straggler, and log_line the relinked entry. The MCP rename_note tool returns the same object plus verification. A warnings entry is {kind, message, …}: frozen_prefix_already_diverged adds path (an archived referrer that already failed its archival check, rewritten anyway), old_slug_already_an_alias adds holder (the old slug is already another note’s alias, so it will resolve to nothing once shared), and sync_check is a warning of the sync check (a failed fetch, no upstream, a state git could not report, an override that let a behind vault through), on a dry run and a write alike. A changes entry is links (with count), field (with key), alias or log_line. A refusal prints the usual {code, message, details} object and exits 1, writing nothing:

codedetailsWhen
invalid_slug, slug_takenas aboveThe new slug breaks the rule, or another note holds it.
note_not_renamablepath, reasonreason is type <name>, archived, folder, rename the question, or what stops the alias being recorded.
stem_sharedslug, path, othersAnother note has the same file stem, so links to it cannot be told apart.
stale_planexpected, actualThe vault changed between the preview and the write. Preview again.
reference_driftpath, driftRewriting the references in path would change more than them.
reference_unaddressablepath, keyA reference field names the note in a form the rewrite cannot address.
rename_incompletepaths, sourceA write failed and the rollback could not restore the paths in paths; check them by hand.
behind_upstreambehind, remedyThe vault is behind commits behind its upstream (after a fetch; for a dry run, as last fetched). remedy says what to do: sync first, or pass --skip-sync-check.
git_operation_in_progressreasonA git operation is paused mid-way (reason names it, e.g. .git/MERGE_HEAD present). Not overridable: finish or abort it first.

search --json → an array of hits

Ranked best-first; each hit:

[
  {
    "path": "portfolios/sparse-vs-dense-attention-ood/2026-03-15-chen-2025.md",
    "note_type": "evidence",
    "title": "Chen et al. 2025",
    "snippet": "...4x speedup at 95% accuracy on the OOD split...",
    "score": 1.7
  }
]

A lower score ranks earlier (best match first). No matches → [].

open --json → the resolved path

cdno open today --json
{ "path": "/home/you/vault/journal/2026/daily/2026-08-21.md" }

Absolute, so the value composes from any directory. A reference that resolves to nothing — or to more than one note — is an error, not an empty result.

open --list --json → an array of candidates

Every note in the vault, most-recently-modified first. The plain-text form of the same listing is tab-separated for piping into a fuzzy finder; --json is for programmatic consumers.

[
  {
    "path": "projects/surrogate-model.md",
    "title": "Surrogate model",
    "type": "project"
  }
]

title is the note’s body H1, and is null for a note that has none.

list verbs → arrays of summaries

cdno project list --json
[
  {
    "slug": "surrogate-model",
    "status": "active",
    "state_snippet": "Mesh scaling works; assembly is the bottleneck",
    "top_action": { "text": "Profile the assembly step", "energy": "medium" }
  }
]
  • project list --closed → [{ "slug", "title", "context", "outcome", "closed_on" }], most recently closed first; outcome is the closure, completed or dropped, and never a parent. This row is the CLI’s own: MCP list_projects with include_closed returns closed maps as { "slug", "frontmatter" }.
  • portfolio list → [{ "slug", "question", "evidence_count", "last_updated", "staleness_days" }]
  • stewardship list → [{ "slug", "name", "context", "variant", "tracking_count" }]
  • action list → [{ "text", "energy", "attached": { "slug", "status" } | null }]

show verbs → a detail object

cdno project show surrogate-model --json   # same shape as a project-list element
cdno portfolio show --portfolio sparse-vs-dense-attention-ood --json
{
  "slug": "sparse-vs-dense-attention-ood",
  "question": "Sparse vs dense attention OOD",
  "created": "2026-03-01",
  "project": "[[projects/surrogate-model]]",
  "evidence": [
    {
      "path": "portfolios/sparse-vs-dense-attention-ood/2026-03-15-chen-2025.md",
      "created": "2026-03-15",
      "source": "Chen et al. 2025",
      "origin": "[[projects/surrogate-model]]"
    }
  ]
}
  • project is null when the portfolio isn’t linked to one.
  • An evidence entry gains a "kind" field (e.g. "pdf") only for attachment stubs; it’s omitted for plain prose notes.
  • stewardship show → { "slug", "name", "context", "variant", "body_markdown" }.

orient --json → the orientation object

cdno orient --json
{
  "commitments": [],
  "projects": [
    {
      "slug": "surrogate-model",
      "status": "active",
      "context": "work",
      "core_question": "[[questions/research/surrogate-generalises]]",
      "state_snippet": "Mesh scaling works; assembly is the bottleneck",
      "top_action": { "text": "Profile the assembly step", "energy": "medium" },
      "held_by": null,
      "counts": true,
      "waiting_on_empty": true
    },
    {
      "slug": "mesh-study",
      "status": "active",
      "context": "work",
      "core_question": "[[surrogate-generalises]]",
      "state_snippet": "Coarse-mesh run validated end to end",
      "top_action": { "text": "Write up the convergence plot", "energy": "light" },
      "held_by": "the modelling group",
      "counts": false,
      "waiting_on_empty": false
    }
  ],
  "groups": [
    {
      "parent": { "kind": "question", "path": "questions/research/surrogate-generalises.md" },
      "projects": ["surrogate-model", "mesh-study"]
    }
  ],
  "slots": { "active": 2, "counted": 1, "cap": 5, "counted_slugs": ["surrogate-model"] },
  "lapsed_habits": []
}

projects holds one project-list element per active project; each carries core_question, the map’s frontmatter value exactly as written, or null when it sets none (project list and project show elements carry it too). Each also carries three keys about the cap (project list and project show elements too): held_by is the name of whoever else is moving the map, trimmed, only when the map’s held_by: names someone else, and null when the map is yours (no held_by:, or me, tbd or another entry of holder_sentinels) or the value is unreadable (not a plain string); counts is true when the map holds one of the max_active_projects slots, that is, it is active and does not name someone else as its holder (branch on counts, never on the name); and waiting_on_empty is true when the map’s ## Waiting On is missing, empty, or only (nothing yet). MCP get_orientation projects, get_weekly_context.projects and get_monthly_context’s parked and stuck entries carry the same three keys; the weekly ones add changed_this_week and focus_days_while_held (see the review fields). groups arranges the same projects, by slug, under the core question they share: a short and a full link to one question group together, since the key is the question’s vault path (or the link’s raw target when it resolves to no note), and only the field counts, never a passing mention of the question in a map’s body. Every project falls in exactly one group, groups follow the order of their first member, and a project with no question is a group of its own with "parent": null. MCP get_orientation carries groups in the same shape. slots is the active projects against the cap: active counts the maps with status: active, counted those holding a slot (the active maps whose counts is true), which is the number the cap is checked against, cap is the configured max_active_projects, and counted_slugs names the counted maps. MCP get_orientation carries slots as { "active", "cap" }, the object list_projects returns.

Casing

Enumerations serialise in their canonical lowercase/kebab form, matching the MCP DTOs: status → active/parked/completed/dropped, energy → deep/medium/light, stewardship variant → flat/expanded, context → work/side-project/household/… So a value is identical whether you read it from cdno --json or over MCP.

Colour and interactivity

cdno’s human-readable output is coloured, and several read commands offer to open one of the rows they just printed. Both behaviours are for people at a terminal, and both switch themselves off the moment the output is going somewhere else.

Cards

Listings whose items carry prose — project list, portfolio list, stewardship list, questions, search, and the active-projects section of orient — render as cards: a coloured bar down the left of every line an item owns, the identifier as a title, a badge aligned into a shared column, and the text wrapped underneath.

3 active projects

▎ surrogate    side-project
▎ Six contributors settled; scope is fixed to the solver rather than the
▎ mesher, and the validation plan is agreed.
▎ next: Draft the validation plan

▎ mesh         work
▎ Coarse-mesh run validated end to end; two workstreams in flight.
▎ next: Profile the assembly step

What the bar’s colour means depends on the listing, and it is always the thing that listing exists to surface: context for project list and orient, staleness for portfolio list and stewardship list (amber once nothing has been filed for a month), and domain for questions. search colours every hit alike — relevance is already the ordering.

Commands whose rows are genuinely tabular keep their tables — status, commitments, orient’s commitments section, and a portfolio’s evidence list all have short, aligned fields where a column beats a card. show commands keep their plain line shape too: a bar earns its space by marking where one item ends and the next begins, and a detail view has only one item.

note list is untouched. It prints one bare path per line because it exists to be piped into other tools, and a header or a gutter would break that.

Colour

SettingEffect
--color autoDefault. Colour only when stdout is a terminal.
--color alwaysColour even when redirected — for cdno project list --color always | less -R.
--color neverNever colour.

--colour is accepted as a spelling of the same flag.

Under auto, three environment variables are consulted, in this order:

  1. NO_COLOR (set and non-empty) turns colour off.
  2. CLICOLOR=0 turns colour off.
  3. CLICOLOR_FORCE (set and non-empty) turns colour on even when redirected.

NO_COLOR deliberately outranks CLICOLOR_FORCE: NO_COLOR is something you export once as a preference about your own terminal, while CLICOLOR_FORCE is usually set by a harness that has no standing to override it. An explicit --color outranks all three.

--json output is never coloured, whatever any of the above says. Scripts can pass --color always safely.

Plain output

cdno open is deliberately outside all of the above. It prints one absolute path, and --list prints one tab-separated row per note — no cards, no colour, no alignment, whatever the terminal is. Both are written to be consumed by another program ($(…), fzf, cut), and a colour escape or a padded column would corrupt them. The tab is load-bearing: it is what fzf --delimiter='\t' splits on.

Interactive reports

In a terminal, project list, portfolio list, stewardship list, orient, status, and search follow their output with a picker:

? Inspect a project
❯ surrogate (side-project)
  mesh (work)
  garden (family)
[↑↓ to move, enter to inspect, Esc to leave]

Choosing a row prints exactly what the matching show command would print, then asks again. Esc or Ctrl-C leaves, with exit status 0.

search is the exception, and deliberately: choosing a hit opens it in your editor and the command ends there, rather than returning to the list. Once an editor has the file, coming back to the search results is not what anyone wants. cdno open’s own picker behaves the same way.

The listing is always printed first, so the prompt only ever adds to what you would have seen. It is skipped entirely when:

  • either stdin or stdout is not a terminal — piped, redirected, < /dev/null, a background job, or running under CI, which is the usual reason neither end is a terminal,
  • --no-interactive is passed,
  • --json is passed, or
  • the terminal is narrower than 20 columns, which is too narrow to draw a picker in.

Both streams matter: the listing is written to stdout but the picker reads stdin, so a caller with a terminal on only one end is not offered the prompt. There is no separate CI detection — a CI job has no terminal, and that is what actually decides it.

That makes the same command safe for a person, a shell pipeline, and an AI agent without changing anything about how it is invoked.

Width

On a terminal, output is laid out to the terminal’s width as measured when the command runs. Text with nowhere to break — a long URL, a long slug, or Thai and Lao, which need dictionary-based word segmentation cdno does not yet do — runs past the edge rather than being cut, on the grounds that a path you can copy beats a path that fits. cdno prints once and exits, so resizing afterwards does not reflow anything already on screen — run the command again. Everywhere else — piped, redirected, or when the terminal reports no usable size (a pty opened without one reports zero columns) — it lays out to a fixed 100 columns, so captured output is deterministic and diffable.

Text from your notes is sanitised before it is laid out — in listings and in show views alike, and in card titles as well as bodies. Tabs and carriage returns become spaces; other control characters, including escape sequences, are replaced. A note is data, and without this a stray escape in a note could repaint the terminal, move the cursor, or draw over the card’s own gutter. The raw markdown is of course untouched, and cdno note still prints paths verbatim.

Frontmatter fields

Every note begins with a YAML frontmatter block. Cuaderno parses it into a typed structure — if it parses, it’s valid (see Business rules). This page lists the fields per note type. ? marks an optional field.

Notes that cdno creates are already well-formed. You mainly need this when hand-authoring or migrating notes — and cdno normalise will reorder keys to the canonical order for you.

daily

type: daily
date: 2026-04-25
tags: []          # auto-populated

weekly

type: weekly
week: 2026-W17
date_start: 2026-04-20
date_end: 2026-04-26

monthly

type: monthly
month: 2026-04
date_start: 2026-04-01
date_end: 2026-04-30

project

type: project
context: work          # work | side-project | university | family | household | legal | personal
status: active         # active | parked | completed | dropped
created: 2026-04-25
core_question?: "[[questions/research/surrogate-cost]]"
held_by?: "the harbour office"   # who else is moving it; set by hand-off, cleared by take-back
closed: null           # set by complete/drop, cleared by activate

status and closed are owned by the lifecycle verbs (park, activate, complete, drop), so cdno frontmatter set refuses both. status has to match the folder the map lives in: projects/ for active, projects/_parked/ for parked, and projects/_done/<year>/ for completed or dropped. A map written before closed: existed needs no edit, since a missing closed: reads as open.

held_by (optional, on an active or parked map) names whoever else is moving the project. An active map that names a holder does not count toward the cap; the map keeps its status and folder. cdno project hand-off writes it quoted and take-back writes held_by: null, each logging holder on [[<slug>]], so cdno frontmatter set refuses it. When you edit it by hand:

  • A missing line, null, a blank value, or a value on [projects] holder_sentinels (me, tbd, someone, …) means no holder: the map is yours and counts. cdno lint warns on a sentinel or blank value.
  • A value that is not a plain string (an unquoted [[people/ana]], which YAML reads as a list, a number, a date) also means no holder, and cdno lint reports it as an error. Quote a wikilink: held_by: "[[people/ana]]".

action

type: action
status: active         # active | completed | blocked | dropped
project: surrogate-model
energy: deep           # deep | medium | light
milestone?: "[[...]]"
due?: 2026-05-10
criteria?: "Definition of done"
blocker?: "What it's waiting on"
created: 2026-04-25
completed?: 2026-05-01
tags?: []

portfolio

type: portfolio
question: "Sparse vs dense attention OOD"
created: 2026-03-01
project?: "[[projects/surrogate-model]]"

evidence

type: evidence
created: 2026-03-15
source: "Chen et al. 2025"
portfolio: sparse-vs-dense-attention-ood
origin: "[[projects/surrogate-model]]"
kind?: pdf             # only for attachment stubs (pdf | image | video | …)

stewardship

type: stewardship
context: personal      # one of the life-domain contexts

tracking

type: tracking
stewardship: health
activity: gym
date: 2026-04-06
duration_min?: 60
routine?: "[[stewardships/health/routines/upper-body-a]]"

question

type: question
domain: research       # research | life
status: active         # active | parked | answered | retired
created: 2026-04-25
updated?: 2026-05-01

commitment

type: commitment
status: active         # active | completed | dropped
due: 2026-06-01
created: 2026-04-25
completed?: 2026-06-01
context: personal
project?: icml-paper
stewardship?: finances

Extending schemas

You can require additional fields per type via config.toml ([schemas.<type>] extra_required) — see Configuration reference. Required built-in fields are always enforced on top.

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

Custom note types

Cuaderno ships twelve built-in note types (see Note types). If you need an entity the built-ins don’t cover — people, books, clients, recipes — you can declare your own custom note type under [note_types.<name>] in .cuaderno/config.toml. No recompile, no plugin.

What a custom type is (and is not)

A custom type is schema-only. It gives you:

  • a folder its notes live in,
  • enforced required / optional frontmatter fields (checked by cdno lint),
  • an optional template,
  • canonical frontmatter ordering (cdno normalise),
  • full participation in indexing, full-text search, backlinks, and cdno note;
  • a creation line in the daily log (<type> created [[<folder>/<slug>]] — <title>);
  • in-place revision with a logged reason (cdno note revise, or revise_note over MCP), unless the type is declared append_only = true.

It does not get bespoke behaviour. The 5-project cap, project state history, commitment aggregation, tracking streams, and action lifecycle belong to specific built-in types and are not available to custom types. A custom type is therefore invisible to cdno orient, the project cap, and the commitments view — by design. If you need that behaviour, use (or extend) a built-in type.

Declaring a type

[note_types.person]
folder = "people"            # required — vault-relative, must not be a built-in folder
required = ["name"]          # fields that must be present and non-null (lint errors otherwise)
optional = ["role", "org"]   # fields that may be present; part of the canonical order
template = "person.md"       # optional; defaults to "<name>.md" under .cuaderno/templates/
append_only = false          # optional; `true` makes `note revise` refuse the type (lint does not check it)
title_field = "name"         # optional; which frontmatter field holds the display title (default: the H1)
date_field = "met_on"        # optional; which field carries the note's date (for date-filtered search)

Validation runs at vault-open, so a malformed declaration fails fast. Cuaderno rejects a type whose:

  • folder is empty, has surrounding whitespace, escapes the vault (.., absolute, \), or collides with a built-in folder (projects, journal, …) or another custom type’s folder;
  • template is not a bare filename;
  • title_field / date_field names a field that isn’t in required/optional;
  • name shadows a built-in type (project, daily, … — case-insensitive). Built-in names are reserved so a stray type: typo can’t silently mint a type.

Bundled types

Some custom types ship with cdno itself. They are ordinary declarations, not built-ins: cdno init writes them into a new vault’s config, and you may edit or delete them like any type you declared. Today there is one, concept.

A vault created before a bundled type existed does not have it. Install it with

cdno config note-type install --list            # what ships, and whether this vault has it
cdno config note-type install --name concept

which writes the type’s template, folder and declaration, each only when absent, and never changes a declaration you already have. See Installing a bundled note type for the report and the refusals.

Creating notes

cdno note create person --title "Ada Lovelace" --field name=Ada --field role=advisor
cdno note list person

--field name=value is repeatable; each key must be a declared required/optional field, and every required field must be supplied. --var name=value supplies a template’s prompted variables.

The note is written to <folder>/<slug(title)>.md. Creation is logged to today’s daily note as <type> created [[<folder>/<slug>]] — <title>, so do not log it again by hand.

The template is looked up in .cuaderno/templates/ under the name template gives, or <name>.md when the declaration names none. There is no built-in fallback for a custom type: if that file exists (.cuaderno/templates/person.md), it is rendered; otherwise Cuaderno synthesises a minimal note — a frontmatter block of your fields plus a # <title> heading — so a type works before you author its template. (Field values are always emitted as strings, so a value with a colon, #, or newline round-trips safely; author a template if you need richer frontmatter shapes for keys you do not pass as fields.)

Whatever a template renders, the frontmatter that is written carries type: <type> and every field you passed (including --origin) with the value you passed: a string equal to it, or a plain number, boolean or null that reads back as the same text (so priority: {{priority}} with 5 stays the number 5). If the rendered frontmatter already does, the note is written exactly as rendered. Otherwise Cuaderno repairs it — a missing field is appended, an unquoted origin: {{origin}} that YAML would read as a list (or reject) is rewritten as a quoted string — and re-serialises just the frontmatter block, which drops that block’s comments and quoting style; the note body is left as rendered. So a template that forgets {{origin}}, or renders no frontmatter at all, still records every field you supplied.

If the rendered frontmatter is not valid YAML at all (for example an unquoted origin: {{origin}} given two links), it cannot be repaired key by key, so Cuaderno rebuilds it: type, then the type’s declared fields in declared order, each with the value you passed or, for title, slug, created and date, the value create computes. The note body is again left exactly as rendered. Any key that only the template wrote is lost in that case. The simplest way to avoid it is to leave origin out of the template and let the repair add it.

The body (--body-file, or body over MCP) is written without its title heading: the engine writes the # <title> H1, and a leading # <title> line in the body is dropped. It fills the template’s {{body}} placeholder, or is inserted after the H1 when there is none. The body wins over any other value of {{body}}; without one, a declared body field, a [variables] value or a prompted body fills the placeholder as usual, and it renders empty only when nothing does. The body is substituted as raw text, so keep {{body}} in the note body: in the template’s frontmatter a multi-line body can add or break YAML keys, and nothing refuses that.

From an MCP client, the equivalent tool is create_custom_note ({ type_name, title, fields, vars, body, origin }).

Discovering placeholders and searching

  • cdno templates vars person lists the {{placeholders}} a person template may reference — its create-path built-ins (title, slug, created, date, body) plus your declared fields.
  • cdno templates eject person does not apply — a custom type has no built-in template to materialise; author .cuaderno/templates/person.md by hand.
  • cdno search <query> --type person filters results to that type. --type accepts any built-in or custom name; a name that is neither errors with the valid set. Shell completion offers your vault’s types.

Relationship to [schemas.*]

[note_types.<name>] defines a type: its folder, its required and optional fields, its template. [schemas.<name>] attaches typed field declarations to a type, and it keys a custom type as well as a built-in. A name under [note_types] may not be a built-in.

For a custom type, a [schemas.<name>.fields.<field>] declaration (see Configuration reference) does this:

  • Lint type-checks it. When the field is present in a note, cdno lint warns if its value does not match the declared type (or values), e.g. field `difficulty` is not a valid int for note type `concept` . A warning, never an error. A value passed with --field (or fields) at creation is always written as a string, so a typed int, float or bool field set that way draws this warning; set it afterwards with cdno frontmatter set, which writes the declared type.
  • settable = true makes it writable with cdno frontmatter set or the set_frontmatter MCP tool, type-checked and without a hand edit. The key must already be in the note’s frontmatter: the setter rewrites a field, it does not add one.
  • list_note_types reports it in the type’s fields, so an assistant can see it.

Two parts of a schema do not apply to a custom type:

  • extra_required is not enforced. A custom type’s required fields come from its own required list, and that is what cdno lint and creation check. An extra_required name on a custom type only shows up in list_note_types, as a string field in fields. Put the field in required.
  • A declared default is not written at creation. A custom note’s frontmatter carries what the template renders plus the fields you pass; to give every new note a field, write it into the template.

The schema does not widen what --field accepts either: a key you pass at creation must still be listed in the type’s required or optional.

Reading and revising

Any note of a custom type can be read whole with cdno open (<type>:<slug> works as a reference) or the read_note MCP tool, and refined in place with cdno note revise or revise_note: either the whole body or one section at a time, with a required reason that is logged as revised [[<path>]] — <reason>. Built-in types are refused (their sections belong to their own commands), and so is a custom type declared append_only = true.

Worked examples

Tracking people walks a person type end to end — declaring it, creating people, and linking them from your notes to answer “what was my last interaction with X?”.

The concept library is the bundled type cdno init declares for you (cdno config note-type install --name concept adds it to an older vault), and Building a concept library walks its creation with --body-file and --origin, and its revision.

Recurrence syntax

Periodic commitments on a stewardship (--every on cdno stewardship add-periodic, or recurrence on the MCP add_periodic_commitment tool) use a small canonical vocabulary.

ValueMeaning
dailyEvery day
weeklyEvery week
monthlyEvery month
every N monthsEvery N months (e.g. every 3 months for quarterly)
yearlyEvery year

Quote any value containing a space on the command line.

Examples

cdno stewardship add-periodic --stewardship health   --title "Dental check-up"     --every "every 6 months" --next 2026-09-01
cdno stewardship add-periodic --stewardship finances --title "File quarterly taxes" --every "every 3 months" --next 2026-07-15
cdno stewardship add-periodic --stewardship health   --title "Annual physical"      --every yearly           --next 2026-11-01

Each line becomes a row in the dashboard’s ## Periodic Commitments section and feeds the aggregated cdno commitments view, advancing to its next occurrence as dates pass.

See also

Troubleshooting

Common situations and how to resolve them.

“not inside a Cuaderno vault”

A command can’t find a vault. Cuaderno discovers one by walking up from the current directory for a .cuaderno/ folder, then falls back to CUADERNO_VAULT_PATH. Fix one of:

cd ~/notebook                      # run from inside the vault, or
cdno --vault ~/notebook <command>  # point at it explicitly, or
export CUADERNO_VAULT_PATH=~/notebook

See Initialise a vault.

The index is a cache; it’s reconciled automatically each run, but a large external edit or a sync conflict can occasionally confuse it. Rebuild it:

cdno reindex

The Markdown files are always authoritative, so a rebuild is safe. See Business rules.

lint is failing

Run it to see the specifics:

cdno lint            # errors fail; warnings are listed but non-fatal
cdno lint --strict   # warnings fail too

Errors are usually an unknown type: or invalid frontmatter; warnings are typically broken wikilinks. Fix the reported file, or run cdno normalise if the issue is field ordering. See Frontmatter fields.

cdno now says nothing is started, but I started something

Three causes, and cdno lint tells the first apart from the rest.

If you wrote the log line by hand, it is almost certainly not in the shape the readers accept. It has to be exactly:

- **09:30**: started [[surrogate-model]] — Draft the methods section (deep)

The - bullet and the **HH:MM**: stamp are what make it a log entry at all, and the separator must be a real em dash (U+2014), not a hyphen. A near-miss is skipped in silence by design — prose beginning “started something” must never register as a focus — so cdno lint reports it instead, naming the line and the likely cause. Run it and fix what it points at. The same goes for paused, resumed and action promoted on lines.

Otherwise, check the date. The focus is read from today’s daily note and the [focus] carry_over_days days before it (default 1, so yesterday’s too); a start logged earlier than that and never closed has expired. Pick the work up with cdno action resume (it also finds a recent pause), or start it again.

The third cause surprises people who remember the older behaviour: focus is one slot. If you started X, then started Y, and finished Y, nothing is in focus. X was displaced the moment Y began, and closing Y does not bring it back, because nobody chose to go back to it. Start X again, or, if you paused it along the way, cdno action resume. Logs written before the slot rule read the same way, so a day that used to report X as the focus now reports none.

cdno now says I’m still on yesterday’s thing

The focus survives midnight on purpose: a start left open yesterday is still the focus this morning, with the day named (since Saturday 14:05). That is the one-day window, carry_over_days. You have three ways out:

  • It is still what you are doing: cdno action resume re-anchors it to today (picked up 08:50 today (started Saturday 14:05)) and keeps it from expiring tomorrow.
  • You stopped but will come back: cdno action pause --next "…", and resume brings the hint back.
  • You do not want anything to carry overnight: set carry_over_days = 0 in .cuaderno/config.toml and the focus is read from today’s note only. A running MCP server keeps the config it started with, so restart it (reconnect the client) for an agent to see the change.

Finishing it with cdno action complete or drop clears it as ever.

action start refuses with focus_open

Something is already in focus, and starting a second thing would leave the log silent about the move. The message names what is open and the command to run:

Error: Draft the methods section is already in focus on surrogate-model (since 09:30).
To move on to this instead: cdno action switch --project other-project --query CI
Or complete or pause it first.

cdno action switch pauses what is open and starts the new action in one step; cdno action pause then start does the same in two. If the open focus is the action you are trying to start, start says so: nothing to do when it was started today, and cdno action resume when it was carried over from an earlier day.

This is most often noticed the first morning after upgrading. A start left open yesterday and never closed is now carried into today, so the first start of the day is refused where it used to stack silently. Resume it, pause it, complete it, or switch. If you would rather keep the old behaviour, set carry_over_days = 0 (and restart a running MCP server, which keeps the config it started with).

Over MCP the refusal is a focus_open rejection with a remedy; see Write tools.

A prompt appears when I wanted automation (or vice versa)

Write commands prompt for missing required flags only in an interactive terminal. In scripts, pipes, or CI they error instead. Force non-interactive behaviour explicitly:

cdno project create --title "X" --context work --no-interactive

Conversely, if a command errors about a missing flag when you expected a prompt, your stdout probably isn’t a TTY (it’s piped or redirected). See CLI overview.

Can’t create a sixth project

That’s the five-project cap: five projects count, that is, are active and name no one else as their holder. Free a slot first: park an active project, or close one that has finished or is not going to happen.

cdno project park --slug some-active-project       # or: project complete / project drop
cdno project activate --slug the-one-you-want

(New projects created while at the cap are created parked rather than rejected.)

My handed-off project still counts

A project you handed off is counted again when its held_by: does not read as a name. Run cdno project show <slug>: it ends with Counts toward the cap: yes or no, and prints Held by: <name> only when the holder is honoured. cdno lint names the cause. The three usual ones:

  • A sentinel. The value names you or nobody: me, myself, (mine), someone, tbd, ?, … A map with such a value is yours, so it counts, and lint warns. Name who really has it with cdno project hand-off --slug <slug> --to "<name>". The list is the vault’s [projects] holder_sentinels; if the holder’s real name is on it (added when you replaced the default list), take it off.
  • A placeholder. A blank held_by: names nobody under any list, whatever the config says. Fill it in, or delete the line.
  • An unquoted wikilink. held_by: [[people/ana]] is a YAML list, not a name, so the holder is not honoured and lint reports an error. Quote it: held_by: "[[people/ana]]". The same goes for a number or a date.

If you cannot name who has the project, it is yours, and counting it is right. Handing it off is not a way to make room at the cap: park, complete or drop a project instead.

The reverse also happens: held_by: "<your own name>" does not count, because cdno cannot know your name. Take the project back (cdno project take-back --slug <slug>), and add your name to holder_sentinels if you want it refused in future.

Writes are refused with a method version error

The vault records the version of the Research Logbook Method it follows ([vault] method_version, see configuration), and every write checks it under the write lock. Reads keep working; only writes are refused, when the major version differs:

  • “…an older major version than the RLM … this cdno implements”: the vault needs migrating. Run cdno migrate --dry-run to see what changes, then cdno migrate. Migration is CLI-only, so an MCP client hitting this error needs you to run it in a terminal.
  • “…a newer major version than the RLM … this cdno implements — upgrade cdno to write to it”: another machine, or a newer cdno, migrated the vault. Upgrade this cdno (and restart any MCP server running the old binary).

A minor difference never refuses a write; it prints a one-line notice on stderr instead. Do not edit method_version by hand to silence either error: the stamp goes with the migration’s log entry and any file steps.

After cdno note rename every link in the vault is rewritten to the new name, and the old name is kept on the note as an alias so cdno keeps resolving it. A link that was written with the old name afterwards (typed from memory, or brought back by a sync merge from a machine that had not seen the rename) still works in cdno, but an editor such as Obsidian or Neovim follows the file name, not the alias, and cannot open it.

cdno lint finds them: [[old]] names a renamed note — now `new` . Leaving them is harmless, since they still work in cdno. To rewrite every one to the current name in one go, run cdno note rename new --relink (--dry-run first to see them; over MCP, rename_note with relink: true): nothing moves, the old name stays listed, and a relinked line is logged. This works for an archived action note too, which cannot be renamed. A union merge brings back the whole hunk, so the lines it duplicated stay after the relink, as identical text twice: the relink corrects addresses, it does not tidy the merge. Changing one link by hand is fine too, even in a past log entry: it is the same address correction.

Rename refused: behind upstream

Error: the vault is 2 commits behind its upstream; sync the vault first (pull the upstream's commits, e.g. with `git pull`), or pass `--skip-sync-check` to rename anyway

cdno note rename will not rewrite many notes in a vault whose other clone has already changed them. Sync the vault (git pull, or your sync tool) and run the rename again. If you are sure nothing unpushed on the other side touches these notes, pass --skip-sync-check (over MCP, skip_sync_check is for the person to decide, not the agent). cdno note rename --dry-run --skip-sync-check shows the plan of a vault that is behind without writing.

If the message is instead a git operation is in progress in the vault (…); finish or abort it, then rename (code git_operation_in_progress with --json; text mode prints no code), a merge, rebase or cherry-pick was left paused in the vault. Finish or abort it (git status says which); --skip-sync-check does not override this one.

--json output won’t parse

--json is only honoured by read verbs and the write verbs that emit a result; maintenance and interactive commands (init, lint, reindex, normalise, triage, review, weekly) ignore it. Under --json, write verbs run non-interactively, so there are no prompts mixed into the output. See JSON output.

Claude doesn’t see the tools

For the MCP server (cdno-mcp):

  • Make sure cdno-mcp is on the client’s PATH, or use an absolute path in the config command.
  • Set CUADERNO_VAULT_PATH (or rely on working-directory discovery).
  • Restart the client after editing its MCP config.

See Connect to Claude.