Skills · cnotes

CreatorNotes CLI (`cnotes`)

Work with CreatorNotes via the cnotes CLI

Installed by cnotes init. Raw markdown: /skills/cnotes/skill.md

cnotes reads and writes a CreatorNotes workspace: typed notes with version history, the relationships between them, and canvases that arrange them so a reader can follow the argument. This file holds what every task needs. Task detail lives in references/; the table at the end says which file to open. Flags for any command: cnotes <command> --help. Every command as JSON: cnotes schema --json.

What you are making

Every note and canvas you write is context: what the next reader, a teammate or the next agent session, opens instead of asking the user to explain again. A write is done when that reader can act on it without the user present. It states what is true, what was decided, what is still open, and what each rests on, and leaves out what that reader would not need. Past sessions left context for you in the same way, which is why every task starts by reading. The definition and its reasoning are DECISION-6KM9 (cnotes notes get DECISION-6KM9).

Every write task, in order

  1. Confirm the workspace. cnotes workspace current names it and lists its relationship types. -w <id> targets another one for a single command.
  2. Read before you write. Past sessions and teammates left context for this task. cnotes brief "<task>" returns the notes, canvases and recent changes most relevant to the task. Search notes and canvases separately (they are different corpora): cnotes search semantic "<topic>" and cnotes canvas search "<topic>". cnotes notes get <id> --expand loads a note with the notes it rests on. Before changing a canvas, cnotes canvas read <canvasId>. A Decision or Guide you find may already settle what you were about to propose.
  3. Open a run. cnotes operations begin --prompt "<the change as a past-tense headline, e.g. Added a risks list to the Q1 roadmap>" --json, as its own command; check its exit code before writing (a pipe hides it). Every write refuses to run without one. The response carries typeRubrics, the same brief, and the proposals waiting on you with any that reword the same note as another (read those first, references/drafts.md step 8); -q prints only the run id, with the proposals on stderr. One canvas per run: end it and begin another before writing to a second canvas.
  4. Write to the type's rubric. Pick the type, run cnotes types show <Type>, and author to its rubric. Audience-facing prose (note bodies, banners, pre-reads) goes through references/humanizer.md first; transcripts, quotes and other raw capture are stored verbatim.
  5. Create notes in one batch. cnotes notes create --notes '[{"key":"A","type":"Insight","markdownFile":"./a.md"}, ...]'. Each body starts with a # h1 (the title): a headline of at most 80 characters that names the claim or the object, with no ", so…" / ", because…" clause, no display id and no em dash; the body's first sentence carries the why. The server refuses a note or version whose title breaks this and says what to change. Cross-link inside the batch with [@A: Title](relationship:<verb>). Detail: references/notes.md.
  6. Grade what you wrote. cnotes quality <displayIds...> --fail-below 4, then revise weak notes with cnotes versions create <id> --description "<what changed>" --markdown-file <path>.
  7. Lay out the canvas. cnotes canvas place <canvasId> --spec layout.json packs items without overlap. A spec can carry the notes too (see the fast path below), which folds steps 5 and 7 into one call. For a new canvas, canvas create --spec takes the same spec, so creating the canvas, its notes and its layout is one call; its name follows the title rule in step 5 (a short headline, · between its parts), and the server refuses one that does not. Every canvas opens with a hero: one large untinted text card at the top-left, above everything, holding # <the goal's result> in under 50 characters, with the four-line banner under it at medium. It is the only text legible at zoom-to-fit. Read references/canvas-elements.md (rules 7 and 9) before choosing elements and references/canvas-layout.md before writing the spec.
  8. Read it back. cnotes canvas read <canvasId>: every claim must appear in a note with a display ID, the hero must name the goal's result, and the banner must make sense to someone who was not in the session. A write that touches no canvas gets the same test: cnotes notes get <id> --expand on what you wrote, read as the next session will, without this conversation.
  9. Close the run. cnotes operations end.
  10. Link the result. End your reply with the canvas link (see Surfacing the canvas link).

Reads need no run. A task that is ONE write (a notes update too) needs no begin/end: pass --intent "<headline>" on that command (see the fast path).

Fast path: a routine canvas change

Adding cards, filling or tidying a section, linking two cards or removing one takes two rounds. Each extra call costs the user a model round trip, so batch, and send independent calls together.

  1. Read, in parallel: cnotes canvas read <canvasId> (or cnotes canvas read-section SECTION-n when the request names one), plus cnotes types show <Type> for each type you will write.
  2. Make the change in one call, with --intent. --intent "<headline>" opens an agent run for that one command and closes it after, so the change lands on the timeline as one revertable run with no operations begin/end. A removal gets a before snapshot automatically, so it can be undone. When a run is already open, the write joins it. To add cards, create and place them together:
    {"notes":[{"key":"A","type":"Insight","markdown":"# Reviews wait on one person\n..."},
              {"key":"B","type":"Decision","markdown":"# Add a second reviewer\nRests on [@A: Reviews wait on one person](relationship:depends-on)."}],
     "root":{"kind":"stack","axis":"horizontal","items":[
       {"kind":"item","type":"note","noteId":"@A"},{"kind":"item","type":"note","noteId":"@B"}]},
     "edges":[{"from":"@B","to":"@A","label":"depends on"}]}
    
    Put the new notes in the spec's notes array (the notes create shape) and place each with {"kind":"item","type":"note","noteId":"@<key>"}; edges and anchors say @<key> too. Add --into SECTION-n to append to an existing frame. Each result comes back under notes, graded when the workspace grades: revise only notes below 4. cnotes canvas place <canvasId> --spec spec.json --intent "Added the reviewer bottleneck to the Q1 roadmap" Other single changes take the same flag: cnotes canvas arrange <canvasId> --section SECTION-7 --intent "Tidied the Risks section", cnotes canvas bulk-remove <canvasId> --nodes '["NOTE-4"]' --intent "Removed the duplicate risk card".
  3. Link the canvas from the Link: line, in the same reply.

Fast path: a new canvas

A new canvas with its notes and layout is one write. Read first, in parallel (cnotes brief, cnotes canvas search, cnotes types show <Type> for each type), then create the canvas and place the spec in the same command:

cnotes canvas create "Q1 · Roadmap" --goal "<goal>" --audience "<who>" --spec spec.json --intent "Built the Q1 roadmap canvas"

The spec is the same document canvas place takes, notes array included, and the result carries the placement under layout. Its root is a vertical stack that opens with the hero ({"kind":"item","type":"richtext","size":"large","content":"# <the goal's result>"}), then the medium banner, then the board. The spec is checked before the canvas exists, and a layout the server refuses removes the canvas again, so a bad file, a missing run or a refused layout creates nothing: fix the spec and run the same command again.

Other writes that belong together (notes on two canvases, a change that needs several kinds of command) still go in one operations begin/end run, so the timeline shows one change. Open references/canvas-layout.md only when the layout needs anchors, origins or several sections; the rules above and step 7 below cover the rest.

Rules that hold on every task

  1. The exit code is the verdict. 0 is success. Never decide success from the body, and never re-run a write because a key looked empty: a second notes create duplicates the batch.
  2. Knowledge lives in typed notes. Text cards, lists, section frames, edges and portals arrange and label; they are unversioned, unsearchable and uncitable. If a future reader needs to find, cite or trust it, it is a note. A text card holds at most three sentences (the orientation banner: four short lines).
  3. Name notes with titled, typed mentions. [NOTE-12: Title](relationship:<verb>), with the verb your sentence uses (blocks, depends-on, answered-by). A bare NOTE-12 in prose creates no link and is a defect, in note bodies and on canvas cards. references is for pure citation only. Reuse the workspace's types (cnotes schema ranks them by use; cnotes rel types lists all) before coining one.
  4. Edges are local to one canvas. When an edge connects two notes, write the same relationship as a mention in a note body, or it exists nowhere off that canvas.
  5. Place declaratively, edit in place. Use canvas place / canvas arrange, never hand-computed x/y. Add to a section with place --into SECTION-n (or --section on a single add); tidy one with arrange --section SECTION-n. Change a text card with canvas update-text and a frame with canvas update-section. Deleting and re-adding mints new ids and drops edges and [[SECTION-n]] mentions.
  6. Content changes are versions. cnotes versions create changes a note's body (the # h1 is its title); fetch the current body first and edit that, since people edit between sessions. notes update changes metadata only (type, tags, pin, archive).
  7. Inside a draft, propose and stop. Shared notes change through cnotes draft propose; the hand-over is cnotes draft ask, then stop. Never merge or approve work you built. Detail: references/drafts.md.
  8. Ask before spending something the user cannot get back: cnotes upgrade uses the workspace's one-time trial, cnotes feedback submit files a report in their name, and cnotes types dimension backfill --yes spends one model call per existing note.
  9. Timeline headlines start with a past-tense verb and name the object ("Shipped the public comic canvas", never "V1"). The server rejects milestone headlines that do not.
  10. Canvas names use the interpunct · as a separator, never a dash: "Path to Revenue · Funding strategy".

Decisions

Anything settled in conversation that shapes what we build or say publicly gets its own Decision card, citing the cards it depends on (typed links such as depends-on / grounded-in). If something is stated as decided but no Decision card exists, say so and create one. When a card settles a Question, link the Question with answers, QUESTION-12: Title, so the Question reads answered.

Write the citation in the Decision's body, not only as a canvas edge: [NOTE-12: Title](relationship:depends-on) or [[grounded-in::NOTE-12]]. A references chip and an untyped [[NOTE-12]] do not cite a dependence. Read the rubric first with cnotes types show Decision.

cnotes notes create still creates a Decision that has no typed outgoing relationship, then says so on that result as hint (so .results[].hint survives a jq of the rows), again in the top-level hints array, and prints the same line on stderr, including with --json. The line names the displayId. The check is the relationships the server created: a references chip does not count, and neither does a mention that never becomes an edge. Fix the body with cnotes versions create <displayId>. Do not run create again.

Reading output

Every command follows one contract.

FlagEffect
--jsonRaw JSON. The default whenever stdout is not a terminal.
-q, --quietOne handle per line: displayId, else name, else id.
--fields a,bKeep only these fields; on an envelope it applies to the rows. With -q, tab-separated lines.
-w, --workspace <id>Target another workspace for this command.
--server <url>Another server (default https://creatornotes.app).
ExitMeaningNext move
0success
2auth requiredcnotes auth status (it exits 2 when signed out), then cnotes auth login
3validationfix the input the error names (details.items lists each bad batch item)
4not foundthe hint names the list command to run
5conflictre-read and retry; an open run blocks operations begin
6rate limitedback off and retry
7permission deniedcnotes workspace current
8subscription required (writes on a free workspace)read references/billing.md; offer cnotes upgrade, never run it unasked
  • Reads return data; writes return an envelope. notes list/get/search emit a bare array (one note is .[0]). notes create returns { results, relationshipsCreated, rubrics?, hints? }; read new notes from results. canvas create returns { canvas: { id, displayId, url } } with id, displayId and url mirrored at the top level.
  • Errors go to stderr as { error, code, exitCode, hint }. The hint names the command that fixes it; read it before retrying.
  • notes list says on stderr which source answered and whether the list may be short. With the local copy on, type, tag, text, pin and no-canvas lists come from it, count every note, and include your own writes; read Showing N of M as the real count. A line saying older matches may be missing means exactly that: narrow the list or relay the hint, never report it as the whole. Counting or grouping is cnotes query (references/more-commands.md).
  • cnotes auth status and cnotes whoami exit 2 when no token is stored. That means signed out. Exit 0 means a token is present. Do not treat exit 2 from status as a broken install.
  • No jq needed. cnotes notes list --type Decision -q, cnotes notes create --notes '[...]' -q --fields key,displayId, CANVAS=$(cnotes canvas create "<name>" -q). Use a JSON parser only for nested values these flags cannot reach.

Display IDs (DECISION-12, CANVAS-406, SECTION-7) work everywhere a command takes an id; Convex ids work too.

Getting started

# The npm package is @creator-notes/cnotes. Do not install the unscoped name cnotes.
npm install -g @creator-notes/cnotes
cnotes init
# No browser: init exits 2, prints a sign-in URL, and prints
#   cnotes auth login --token-stdin <<'EOF'
#   cn_...
#   EOF
cnotes auth status             # exit 0 signed in, exit 2 signed out
cnotes whoami                  # same as auth status
cnotes workspace current       # the active workspace and its relationship types
cnotes brief "what this project is about"

After you create a canvas or finish changing one, end your reply with its link, the display id as the link text:

Built CANVAS-321 · Pricing model: 7 notes across 3 columns.

canvas create, canvas get, canvas place, canvas bulk-add and canvas add-node print a Ref: and a Link: line (url in --json). Copy them; never assemble a URL yourself. The only valid shape is <server>/<workspaceId>?canvasId=<Convex canvas id>; paths such as /canvas/<id> or /workspace/<id>/canvas/<id> do not exist.

Where the detail lives

When you areOpen
Writing any prose a person will readreferences/humanizer.md
Creating, reading or revising notes; types, rubrics and dimensions; markdown rulesreferences/notes.md
Linking notes, reading backlinks, checking what an edit affects (deps)references/relationships.md
Deciding what goes on a canvas as a note, text card, list, frame, edge or portal; the banner; completion testsreferences/canvas-elements.md
Placing or rearranging several items (canvas place, arrange)references/canvas-layout.md
Using any other canvas command: read, add, edit, move, remove, edges, templates, milestones, digestreferences/canvas.md
Running two agents at once, bracketing canvas edits, verifying attribution, reading the timelinereferences/operations.md
Reworking notes or a board other people read, or asked to draftreferences/drafts.md
Another agent works in the same workspace: handing work over, reviewing another agent's proposal, overlaps, settling it, when to stopreferences/agents-together.md
Reviewing (propose the fix, or comment), addressing comments, requesting or answering a review, checking what teammates sharedreferences/reviews-and-comments.md
Hitting exit 8, or asked for another workspacereferences/billing.md
Planning focus blocks, handling images, setup, theme, config, or reporting a CLI problemreferences/more-commands.md

Working with other agents

Read this when another agent works in the same workspace as you: a second session of yours, a teammate's agent, or a bot in the same Slack, Teams or Discord channel as you. CreatorNotes is where you find each other's work, hand it over and settle it. A post in a Slack thread (or Teams, Discord, a GitHub comment) is not: some bots never see other bots' posts (Claude in Slack filters them out), and the next agent, in a terminal with no access to that thread, never sees it at all.

Have your own key

CreatorNotes tells agents apart by key. The agent that built a proposal is its key: that key can never prepare or give the answer to it. Two agents on one login key count as one agent, so a reviewer on the builder's key is refused.

  • An agent you start gets a named key from you: cnotes auth keys create --name "Codex (Slack)" --expires 8h -w <workspace> -q, handed over as CNOTES_TOKEN. Its runs show the name in History ("Deniss via Codex (Slack)").
  • Two agents on one machine each set CNOTES_TOKEN, and each pins its own proposal with CNOTES_PROPOSAL=<id> (or none to work live). A proposal id saved on another machine works too.

Start by reading what waits

cnotes operations begin lists, before you write anything:

  • Waiting on you: proposals asked of the person you act for, who built each ("built by you via Codex (Slack)"), whether another agent already prepared an answer, and which reword the same notes.
  • Your proposals that overlap another: yours, not yet asked, that reword a note another open proposal also rewords.

With -q these lines go to stderr. Read them first: the work you were about to start may already be proposed, or already reviewed. You only ever see proposals your person could already see.

Hand work over as a proposal

Hand another agent work as a proposal, never as a post in Slack or another channel outside CreatorNotes. Open it (cnotes draft open "<name>" --intent "<the problem>"), write into it, then ask a person: cnotes draft ask --reviewer <email|me> --headline "...". The person asked is always a person; their agents review it for them. Then stop. Details: drafts.md.

To point another agent at anything else (a finding, a note to check), write it in a note and give the display id: "my results are in OBSERVATION-123". In a Slack thread, post that id, never the finding itself.

Review another agent's proposal

What you may do depends on whose proposal it is:

  • Built by another agent of your own person: you cannot accept or send it back; your person gives every answer to their own agents' work. Prepare the answer instead, after you read it (cnotes draft status <PROPOSAL-n> and its rewordings): cnotes draft prepare <PROPOSAL-n> --accept --note "<why>" or --send-back --note "<what should change>". Your person sees "<your key's name> recommends landing it" with your line and gives it with one tap. A later write into the proposal clears what you prepared.
  • Built by a teammate's agent and asked of your person: you may accept it or send it back under your person's key, as they would: cnotes draft accept <PROPOSAL-n> --note "<why>" or cnotes draft send-back <PROPOSAL-n> --note "<what>". Decide only when your person told you how; otherwise prepare.

When two proposals overlap

draft ask prints "Overlaps: PROPOSAL-7 ... also rewords DECISION-1, built by ...", draft status lists it as a proposal-overlap warning, and operations begin marks it. It never blocks. Read the other one and decide which holds:

  • They agree: say in your ask which should land first.
  • Yours repeats it: fold what is new into the other, or dismiss yours.
  • They conflict: write a Question for the person naming both, with your lean, and stop.

Settle it in a note

When the agents agree on something, one agent writes it down; the others check it. Say "I'll write it" first (in the proposal's ask, a canvas comment, or the Slack thread you share) so two agents do not write it twice.

  • An outcome is a Decision that cites what it rests on (the proposals, the findings) with typed links. It counts as agreed only when the person versions it.
  • A choice only the person can make is a Question with the options and your lean.
  • The other agent reads the note and fixes it with an edit, never a second note.

When to stop

  • Three send-backs between agents is the cap. Between two people, after three send-backs made by agents with no person in between, the fourth is refused (agent_rounds). Write a Question for your person that names what the two sides disagree on, with your lean, and stop. A person sending it back or asking for it in the browser starts the count again. Accepting stays open.
  • Ask, then stop. After draft ask or draft prepare, your part is done until a person answers.
  • Nothing lands without a person you act for or under their key; never press Accept in a browser for work you built.

Read this when a write fails with exit 8 (subscription_required) or the user wants another workspace.

Reads work on any plan. CLI/API writes require the target workspace to be on Pro — this is the most common wall you will hit (exit 8, serverCode: "subscription_required"). There are three distinct walls, and only two of them need a human. Do not collapse them into "tell the user to go pay."

Wall 1 — free workspace, write blocked. You can fix this yourself.

If the caller owns the workspace and hasn't used its one-time trial, cnotes upgrade starts a free 7-day Pro trial and writes unlock immediately. No credit card, no browser, no human hand-off — it's a single command you can run.

cnotes upgrade          # start the free trial on the ACTIVE workspace, then retry the write
cnotes upgrade -w <id>  # target a specific workspace

Ask before running it. The trial is one-time and cannot be restarted, so never fire it unprompted. Offer it in one line and wait for a yes:

"This workspace is on the free plan, so I can't write to it. Want me to start your free 7-day Pro trial (no credit card)?"

On yes: run cnotes upgrade, then retry the write. That's the whole flow.

Wall 2 — trial already used. A human must pay.

cnotes upgrade fails with trial_used. Card entry is browser-only, so you cannot subscribe on the user's behalf. Surface the upgradeUrl from the error (the workspace's billing page, $49/mo), keep doing read-only work in the meantime, and retry the write once they confirm they've paid.

Wall 3 — they want an ADDITIONAL workspace. Human pays at checkout.

cnotes workspace create buys a new workspace — it is not the fix for a blocked write on the current one (that's Wall 1). Only reach for it when the user actually asks for another workspace.

cnotes workspace create "Acme Research" --json          # returns { checkoutUrl, sessionId }
cnotes workspace create --verify <sessionId> --json     # poll after the human pays

The CLI never charges a card: create returns a Stripe Checkout URL a HUMAN must open and pay. Relay checkoutUrl to the user, then poll --verify every few seconds until status is "ready" — the new workspace is set active automatically. --trial starts that subscription with a 7-day trial (a card is still collected at checkout).

If the caller is not the workspace owner, never tell them to run cnotes upgrade — it dead-ends on an owner-only 403. The error hint already branches for this case: ask the owner to upgrade.

Canvas elements: what goes where

Read this before building or changing a canvas. It decides which element holds what, the hard rules, the hero and orientation banner and the three completion tests.

The vocabulary is five elements:

ElementWhat it isCLI
noteA reusable knowledge object: typed, versioned, searchable, with a display ID other notes can mentionadd-node, place type:"note"
textFree-form rich text placed directly on the canvas — markdown (headings, emphasis, images), optional background tint (none by default). Wire type is richtext.add-text, place type:"richtext"
listA container whose content is its MEMBER NOTES; the description is a one-line frameadd-list
sectionA named region frame (Miro-style). Mints SECTION-<n>, mention-addressableadd-section
canvasA portal to another canvasadd-link, place type:"canvas"

There is no separate heading element. A heading is a text element whose content is a markdown heading and which has no background tint: add-text <canvasId> --content '# Q1 Goals'. (An older text node kind — plain string + font size — is retired: existing ones still render and can be moved or deleted, but nothing creates them any more.)

A canvas holds two tiers of content, and the difference is load-bearing:

  • Notes are the knowledge tier: typed, versioned, searchable, citable by display ID — the only tier whose content other notes can mention, search can find, and history records. Anything a future reader must find, cite, verify, or build on lives here.
  • Everything else — text elements, list containers, section frames, edges, portals — is the presentation tier: orientation, grouping, emphasis, navigation. Text elements, lists, edges and portals have no display ID (nothing can cite them), no version history (they mutate silently), and no search presence. (Sections are the one exception: a frame mints a SECTION-<n> you can mention — it addresses a place, not knowledge.) They shape how the knowledge lands; they never hold the knowledge.

The default is a typed note. Before reaching for any other element, apply one test: will any future reader — a human, or the next agent session pointed at this canvas for context — need to find, cite, or trust this? If yes, it MUST be a note. "It renders nicely on the canvas" is never a reason to put knowledge in a text element: that trades permanent legibility (versions, search, citable IDs) for screen presence now. The decision-table rows below are the sanctioned exceptions — when a row matches your content exactly, the row wins.

Decision table

You haveUseNOT
A claim, fact, decision, risk, question, verdict, score, spec, or example worth keepingA typed note (right supertag, # h1 title)a text element
N distinct things (questions, risks, options, findings)N notes grouped in a list; --description = one-line framebullets in one text element or in a list --description
A named REGION of the canvas ("Shaping", "Open Questions")a section frame (add-section) — draggable region, mention-addressable as [[SECTION-<n>]] from note bodiesa floating text heading over an implied area
A heading / one-line label where a frame is too heavy (a column header inside a frame, a lane key)a text element holding a markdown heading — add-text --content '# Q1 Goals', no --color (leave it untinted), title casea tinted card, or a section frame for something that isn't a region
The board's goal, readable at zoom-to-fitone large untinted text hero at the top-left, # <the goal's result>, shaped by rule 7a medium heading standing in for it, the canvas name repeated, a tinted hero
Orientation: what happened, what the board is for, how to read itone medium text banner per canvas, directly under the hero, shaped by rule 7 (a band may add a one-line one)a paragraph banner, a reading-order-only banner, a Guide note
Emphasis — restating a key claim for screen presencea text element that QUOTES a note (the note stays the home)the text element as the only copy
An image tile (logo, mockup, screenshot)a text element holding the image, beside the owning note; embed the same image in the note body via cnotes files upload <path> --markdown so the note stays self-containedthe image as the knowledge itself
A grid of images for review (mood board)N text-element image tiles in a grid place spec + ONE note holding the decision criteria / rationale for the seta caption note per tile, or rationale in the tiles
"A connects to B" between two notesa labeled edge (short verb phrase), notes onlyprose explaining the connection
The exact line from a transcript or note that carries a pointa Quote note (add-quote): the words pinned to that note's version, drawn on the board as a quote, reusable on any canvas as the same QUOTE-<n>; interpretation goes in an Insightpasting the words into a new note or a text element, where they drift from the source
A pointer to another canvasa portal — add-link on the CLI, {"type": "canvas", "linkedCanvasId": ...} in place specs, returned as canvasLinkNodes by canvas geta text element saying "see other canvas"
Backstage narration (scripts, speaker notes)NOTES on a PRIVATE sibling canvas (cue-card text elements are fine there); portal FROM the private canvas TO the shared one, never the reversenarration on the shared canvas

Hard rules (each one has burned a real canvas)

  1. A canvas element must never be the sole home of a claim. Verdict tallies, dates, scan provenance, and even a moat-defining sentence have each been authored as text elements — then cited downstream while being unversioned, unsearchable, and uncitable. If a text element says something true and useful, a note says it first; the text element may quote the note. (Canvas-specific wayfinding — how to read THIS canvas — is the one exemption; it has no off-canvas value.)

  2. Name notes and canvases from text elements with relationship mentions, never bare ids. [NOTE-12: Title](relationship:references) inside a text element renders as a mention chip carrying the target's real title (a list description renders the same mention as a labelled span). "See RISK-64" as plain prose is a defect: the reader gets no title and no link. Pick the relationship type from the verb the sentence uses and reuse the workspace vocabulary. The chip is presentational: it creates no graph relationship, is not searchable, and is not versioned, so a relationship that must survive off the canvas still lives as a mention in a NOTE body (rule 5).

  3. A text element is capped at card size: at most 3 sentences, one heading at most, never a list of distinct knowledge items (questions, risks, findings — those are N notes; a 3-line color legend or lane key is fine). The one exception is the orientation banner (four short lines, rule 7). A heading-only text element (--content '# Q1 Goals', or the hero) is of course fine — that is the smallest legitimate use. The cap is physical as much as doctrinal: a text element renders its full content inline, so long blocks explode the canvas — one canvas hit 43,000px tall from eight text reference blocks and had to be rebuilt as notes.

  4. Detail's HOME is the note body; text detail cards are projections. Write the full spec/content INTO each note's body first. A parallel text card under a note is allowed only as a projection — it condenses or quotes a note that already contains everything, and says so ("from STAGE-3"). A detail card whose content exists nowhere else is the anti-pattern: one teaching canvas carried its entire method in text cards over one-line stub notes, and an agent reading it back got seven stubs and no method.

  5. Edges can connect any node type (note, text, list, canvas-link, section — plus legacy heading nodes), as long as both endpoints are on the same canvas; labels are short verb phrases ("fixes", "grounds", "depends on"). A section endpoint is addressed by its SECTION-n handle (or the frame's node id), and the edge anchors to the frame's border — use one when the relationship's endpoint is genuinely the whole region ("this cluster feeds that stage"), not as a shortcut for a note-to-note link. canvas read returns edges, so they are no longer invisible on read-back — but an edge is still CANVAS-LOCAL: it is not part of the note graph, never appears in rel list, is not searchable, and means nothing once you leave this canvas. So any relationship between notes that must survive off the canvas has to ALSO exist as a mention inside the note bodies. The mention is the copy that travels; the edge is the copy you can see. Edges to text/list/canvas-link nodes are diagram decoration (pointing a label at a cluster, wiring a portal) and carry no note-to-note relationship to mirror.

  6. Color is a ROLE or it is absent. Default neutral — a text element has NO background unless you pass --color, and that is usually the right look (an untinted heading is just type on the canvas). When a tint does mean something, pick from the six role hexes and reuse ONE role per meaning, 2–4 roles per canvas max:

    RoleHexUse for
    Evidence#3B82F6input, quote, data
    Commitment#22C55Edecided or owned work
    Insight#EC4899the aha — 1–2 per canvas
    Warning#F59E0Bcaveat, risk
    Danger#EF4444blocker
    People#8B5CF6person, stakeholder

    Do not hand-pick a hex outside this table: the renderer snaps every tint to the nearest role by hue, so an off-palette color does not get you a new shade — it just picks a role imprecisely (#c2410c lands on Danger, not Warning). A tint colors the CARD only; text inside it keeps the normal prose colors, so the tint can never make a card harder to read. If you cannot name the role, omit the color.

    Neutral is the default, not the goal. A board where no card carries a role hides its caveat and its commitment among cards that all look alike. When the board has a caveat that could sink the plan, a piece of decided work or the one aha, tint those one to three cards by role. The hero and every heading stay untinted.

  7. The hero and the banner open every canvas. A cold reader sees them first, often only them.

    The hero is one large untinted text card at the top-left, above every section and band, holding a single heading: # <the goal's result in a few words>. Nothing else goes in it. It is the one line a person can read at zoom-to-fit: real boards fit the screen at 14 to 29% zoom, where a large heading paints at 17 to 37px, a medium one at 8 to 16px and a small one at 4 to 9px. So a board without a large hero shows nothing legible at the glance.

    • Keep the heading under 50 characters. A large line holds about 25, so 50 is two lines and a card about 600px tall; a full goal sentence as the heading runs to five lines and about 1000px.
    • Name the result, not the board: "Agents read the canvas first", not "Ports canvas" or "Overview". No clever names, and never just the canvas name, which the title bar already shows.
    • The full goal sentences stay in canvas create --goal; the hero is their result, so the two never drift.
    • One hero per canvas. A section or band heading is never large (rule 9).

    The banner sits directly under the hero: one medium untinted text card, no heading (the hero is its heading). Write one line each, in this order, no paragraphs:

    1. What happened. Dated, with names a stranger can look up. "On 19 Sep 2026 Deniss asked why CreatorNotes logs what people open but not what agents read."
    2. What it's for. The decision or artifact the board produces.
    3. Who acts. Who decides or builds, and where the answer goes (a mention chip, rule 2).
    4. How to read it. The cue only: "Read left to right." Don't list each frame's contents; the frames say that. Keep each line under ~20 words. Break long lines; never merge them into a paragraph. Name things before you point at them: no "the ad", "the app", "the scope". Cut: frame counts ("in five moments"), method notes ("fat-marker sketches", "coral marks…"), and recaps of what the strip below says. Wayfind and Attention canvases add their own lines after these four, never instead.
  8. Quotes carry a board's best lines. A board of same-shaped cards hides its point. After the notes are placed, find the 2–4 sentences the board rests on (a customer's own words, the reason inside a Decision, the number that changes the story) and add-quote each from its source note. Each lands beside its source as a Quote note (QUOTE-<n>) that other boards can reuse.

    • Size is hierarchy (rule 9). One large quote per board: the line everything rests on. medium for one to three supporting lines. small for evidence beside the card it backs. Never two large quotes; the hero is the only other large card.
    • Style follows the job. pull for a line that stands alone. highlight when the words need their sentence around them. evidence for data or an excerpt backing a claim next to it.
    • Colour follows rule 6, by meaning. insight for the hero line, evidence for customer words and data, warning for the caveat that could sink the plan, people for a stakeholder speaking. Most quotes stay untinted; tint two at most.
    • Every quote is cited by a claim on the board. A wall of lines nobody interpreted is a pile, not an argument. About one quote per five notes is plenty.
    • Words from outside the workspace (a call, a Slack thread, an AI reply) become a Quote note directly: notes create with type Quote, the words in a blockquote, then a from <who>, <where>, <date> line. Place it like any note and style it with style-quote.
  9. Three sizes make three layers. A text card's size sets its type as well as its width (headings 30, 56 and 128px; body 14, 24 and 48px), so size is how a board says what to read first, second and third. A board where every text card is the same size reads as one flat layer.

    SizeHoldsHow many
    largethe hero (rule 7) and the one large quote (rule 8)one hero, at most one quote
    mediumthe banner, band and column headings, supporting quotesa few
    smallprojections, annotations and lane keys beside the card they speak foras many as the cards need

    Pick the size by reach: a band heading at large competes with the hero, and an annotation at medium outshouts the note it backs. Colour (rule 6) adds the second axis: size says how far a line reaches, the role tint says what kind of line it is.

Three completion tests

Run all three before ending your operation:

The read-back test (for the next agent): run cnotes canvas read and check that every claim is present as a NOTE section with a display ID, and the reasoning can be followed through note bodies and their mentions. Content that appears only as anonymous text-element prose fails the test even when visible — nothing can cite, search, or version it. List members read back as ID+title links without bodies; that is fine — the IDs are citable and batch-fetchable via cnotes notes get. As a heuristic for working canvases, at least ~4 notes per text element (zero text elements is fine); fewer means text elements are carrying content. Explainer canvases using the projection pattern (rule 4) are exempt from the ratio, not from the test.

The glance test (for the human): zoomed to fit, a person should grasp what this canvas argues and where to start reading in about 10 seconds. A canvas that passes read-back but renders as an undifferentiated card grid is also not done — that is what the hero, section labels, orientation banners, projections, and layout are for. Fail it when there is no large hero at the top-left (rule 7) or when every text card is the same size (rule 9).

The cold-reader test. Cover everything but the hero and the banner. Can a stranger say what happened, what the board produces, and who acts? Fail it if the banner only gives reading order, uses unnamed referents, or runs as a paragraph. Test the banner text itself, not your memory of the run.

Worked example

Right shape (a real competitor scan): 31 typed notes (COMPETITOR profile, FACT evidence row, INSIGHT verdicts per dimension, RISK/IDEA/STRATEGY response bands, QUESTION follow-ups in a list), three short text banners framing the bands ("Threats, Steals, Response — what hurts us, what's worth taking, the plan"), and labeled edges mirrored as mentions (FACT-44 --grounds--> INSIGHT-361).

Wrong shape (same content): one 5,000-char text element holding a "deep scan writeup", a text scoreboard holding the verdict tally, and a few notes for leftovers. Search finds nothing, versions record nothing, nothing can cite or link any of it — and the headline verdict goes stale the first time someone edits the card.

Current platform limitations (re-check before relying on these)

The durable rules above stand on versioning, search, and citability. Four rules also lean on today's platform behavior — if a future release changes these, the corresponding rule can relax:

  • Mentions inside text elements and list descriptions are presentational chips only: no graph relationship, no search presence, no versions (motivates rule 1 and the mirror-as-mentions half of rule 5).
  • canvas read returns sections, edges and portals, but list containers still read back as their one-line frame only. An edge read back is still canvas-local and ungraphed (motivates rule 5's mirror-as-mentions requirement).
  • Text cards and section frames are edited in place (canvas update-text, canvas update-section), but neither keeps a version history. Lists have no update verb: changing one means remove and re-add. Content that evolves belongs in notes, which have versions.

Canvas layout with canvas place

Read this before putting more than one item on a canvas. Describe structure; the server measures and packs.

Don't compute coordinates by hand. Use cnotes canvas place with a layout spec.

The server measures every item, packs them into a no-overlap layout, and inserts them in a single batch. You describe structure (rows, columns, what's near what) — the server resolves pixels. This is what cnotes canvas place is for, and you should reach for it whenever you're adding more than one item.

The call is best-effort, not transactional — per-item failures are reported in the response (HTTP 207) but the items that succeeded stay on the canvas. Always inspect items[].error and edges[].error before assuming success.

You don't have to find a free coordinate. place is collision-aware against content already on the canvas. By default (placement: "auto") it drops your layout in a clear band below everything that's already there, and if you do pass an origin that would overlap, it slides the whole block straight down until it's clear. So appending a new section to a non-empty canvas Just Works — no need to read existing positions and guess a safe origin first. Use --placement append to force "below everything", or --placement exact (or "placement": "exact" in the doc) to honor your origin verbatim for pixel-perfect templates.

Use cnotes canvas bulk-add with explicit x/y ONLY for pixel-perfect templates (e.g. demo recordings or tutorial walkthroughs that need to match a specific screenshot).

The layout DSL

A layout document is { root, edges?, origin?, placement? } where root is one of four node kinds:

  • stack — items flow along one axis (like CSS flexbox)
    { "kind": "stack", "axis": "vertical", "gap": "medium",
      "align": "start", "items": [ /* LayoutNode */ ] }
    
  • grid — N columns, items wrap to new rows (like CSS grid auto-flow)
    { "kind": "grid", "columns": 3, "gap": "medium",
      "items": [ /* LayoutNode */ ] }
    
  • anchor — place a sub-tree relative to an EXISTING canvas node
    { "kind": "anchor", "to": "NOTE-12", "direction": "right",
      "gap": "medium", "child": /* LayoutNode */ }
    
    direction is exactly one of "right" | "left" | "above" | "below" — NOT top/bottom/up/down.
  • item — a leaf node (no x/y!). One per visible thing on the canvas.

gap accepts a preset ("tight", "medium", "spacious") or a raw pixel number. Defaults to "medium".

The schema is strict: an unknown or typo'd key anywhere in the doc (e.g. gapp, positionx) is rejected with the offending key named, rather than silently ignored. Validation errors return details.issues with the full path (doc.root.items[2].direction), the allowed values, and what you sent.

placement (top-level, optional) controls where the whole layout sits relative to existing content: "auto" (default, collision-aware — below existing content / de-collide), "append" (always below everything), "exact" (honor origin verbatim). origin is the top-left corner; omit it and auto/append pick a safe one for you.

Leaf item shapes

{ "kind": "item", "type": "note",     "noteId": "NOTE-3", "key": "a" }
{ "kind": "item", "type": "richtext", "content": "# Q1 Goals" }
{ "kind": "item", "type": "richtext", "content": "Read this band left to right.", "size": "medium", "colorHex": "#F59E0B" }
{ "kind": "item", "type": "list",     "description": "Risks tracked weekly — owner: @ana", "noteIds": ["A","B","C"], "viewMode": "list" }
{ "kind": "item", "type": "canvas",   "linkedCanvasId": "abc..." }
{ "kind": "item", "type": "section",  "name": "Open Questions", "width": 800, "height": 500 }

richtext IS the text element (the CLI's add-text) — the only free-form text leaf. A heading is a richtext leaf whose content is "# Title" with no colorHex. There is no "type": "text" leaf: the old heading node kind is retired and the layout engine rejects it.

New notes in the same call. A spec can carry a top-level notes array in the notes create shape ({key, type, markdown|markdownFile, tags?, changeDescription?}). A note leaf places one with "noteId": "@<key>" and takes <key> as its own key, so edges and anchors say @<key>; bodies cross-link with [@<key>: Title](relationship:<verb>). The notes are checked before anything is written, created only if the layout solves, need an open run, and come back under notes in the notes create response shape. A dry run creates nothing. If placing then fails, the notes still exist: fix forward, never re-run the spec.

Leaf enums (exact values): richtext size = small | medium | large, colorHex = a saturated hex (omit for no background); list viewMode = list | grid, list size = sm | md | lg | xl.

A richtext size is not a hint — it IS the width the card paints: small 560px, medium 1120px, large 1680px, with the height following the content. It sets the type too (headings 30, 56 and 128px; body 14, 24 and 48px), which is why the hero is large and an annotation small (canvas-elements.md rules 7 and 9). A large hero under 50 characters is about 600px tall; a four-line medium banner without a heading about 375px. place measures that box for you, which is the whole reason to prefer it over hand-picked coordinates: pitch a row of columns for small and then send medium items and every card runs 460px into its neighbour.

Section container (frame WITH content — preferred)

Beyond the fixed-size leaf above, section is also a CONTAINER: wrap any stack/grid in a named frame and the solver sizes the frame around the content plus padding — no coordinate math, one call. This is how "create a section and lay out notes in it" should be expressed:

{
  "kind": "section", "name": "Open Questions", "key": "oq",
  "child": { "kind": "grid", "columns": 3, "items": [
    { "kind": "item", "type": "note", "noteId": "QUESTION-12" },
    { "kind": "item", "type": "note", "noteId": "QUESTION-14" }
  ]}
}

The place response's section item carries its displayId (SECTION-<n>) — surface it and mention it as [[SECTION-<n>]] from note bodies. For the simple case, add-section --notes id1,id2 is the same thing as a one-liner. To reflow content that ALREADY sits on the canvas into a section (mixed kinds — notes, canvas-links, richtext, lists), use canvas arrange — do NOT hand-compute coordinates with bulk-move + update-section. To grow or rename a frame without moving its contents, use canvas update-section — never delete + re-add, which mints a new SECTION-<n> and breaks every [[SECTION-<n>]] mention pointing at the old one.

Arrange existing nodes (reflow)

place is create-only. When the nodes are already on the canvas, use cnotes canvas arrange:

cnotes canvas arrange <canvasId> --section SECTION-90 \
  --nodes '[{"nodeId":"<id>","nodeType":"canvas"},{"nodeId":"NOTE-3","nodeType":"note"}]' \
  --layout grid --columns 4 --gap medium

The server measures each card from its on-canvas footprint, packs them with no overlap, moves them in one batch, and auto-resizes the target section in place (SECTION-<n> stays stable). --no-auto-resize moves without growing the frame. --dry-run previews positions.

Full DSL alternative: a layout doc whose leaves are { "kind":"item", "type":"existing", "nodeId":"…", "nodeType":"note"|"text"|"richtext"|"list"|"canvas" } (plus an optional section container + --section / targetSectionId).

Arranged sections

A section can lay out its own cards. Every section place creates from a section CONTAINER is arranged, and so is any section you run cnotes canvas arrange <canvas> --section SECTION-<n> on (no --nodes). An arranged section:

  • tidies what is inside it, keeping the rows and columns the cards are roughly in, and sizes the frame to fit (what sits below is pushed down if it grows);
  • re-lays itself out when one of its cards is added, edited or removed: a text card that grows pushes the cards under it down, a deleted card's gap closes;
  • stays arranged until someone MOVES A CARD BY HAND (drag, move-node, bulk-move) or resizes the frame. Then it is freeform again and nothing moves. arrange --section SECTION-<n> --stop does the same on purpose.

Put new cards into a section with the section, never with coordinates inside the frame:

cnotes canvas place <canvasId> --spec ./cards.json --into SECTION-13   # several
cnotes canvas add-node <canvasId> --note RISK-4 --section SECTION-13   # one

They are appended to its layout (the end of its shortest column, or the next cell of a grid) and the frame re-arranges. canvas status marks arranged sections and lists any card that runs past its frame's edge; fix those with arrange --section.

The optional key lets edges and other items reference this leaf later (@<key>).

richtext.content (the text element) accepts markdown directly — the server converts to TipTap before measuring. Markdown including relationship mentions: [NOTE-X: Title](relationship:verb) renders as a chip with the real title, so use one wherever the card names a note or canvas (a bare id is a defect), and keep it card-sized per canvas-elements.md.

list.description is a one-line frame; the content is noteIds, the member notes (N distinct things = N notes, see canvas-elements.md).

Edges

Edges live at the top level alongside root. Each endpoint is resolved in this priority order:

  1. @<key> — any item placed in the same call that carries a matching key
  2. Original noteId of a note placed in this call (e.g. "NOTE-7") — no key needed
  3. Display id or convex id of a node already on the canvas
"edges": [
  { "from": "@a", "to": "@b", "label": "depends on" },
  { "from": "NOTE-7", "to": "@a" }
]

Examples

Bands (a story read top to bottom). When the user asks for "bands", or the canvas makes one argument in order (the ask, the problem, the root cause, the reframe, what we're solving, the grounding), give each step its own band: a heading naming the step's role, with that step's cards in one row under it. CANVAS-99 is the reference board. Gaps 100 / 20 / 140 reproduce its spacing (bands about 310px apart, cards on a 480px pitch).

{
  "root": {
    "kind": "stack", "axis": "vertical", "gap": 100, "align": "start",
    "items": [
      { "kind": "stack", "axis": "vertical", "gap": 20, "align": "start", "items": [
        { "kind": "item", "type": "richtext", "content": "# The Problem" },
        { "kind": "stack", "axis": "horizontal", "gap": 140, "align": "start", "items": [
          { "kind": "item", "type": "note", "noteId": "PROBLEM-25" }
        ] }
      ] },
      { "kind": "stack", "axis": "vertical", "gap": 20, "align": "start", "items": [
        { "kind": "item", "type": "richtext", "content": "# The Reframe" },
        { "kind": "stack", "axis": "horizontal", "gap": 140, "align": "start", "items": [
          { "kind": "item", "type": "note", "noteId": "INSIGHT-125" },
          { "kind": "item", "type": "note", "noteId": "INSIGHT-126" },
          { "kind": "item", "type": "note", "noteId": "INSIGHT-127" }
        ] }
      ] }
    ]
  }
}
  • One band per step, and one row per band. If a row would go past five cards, the step is really two steps, so split it into two bands.
  • Bands are freeform on purpose. Do not wrap them in a section container: a section re-arranges itself, and a bands board is placed once and then edited by hand.
  • To add a band to an existing bands board, place one band with the default placement; it lands below what is there. To reflow the cards already on the board into bands, send the same document to canvas arrange --spec with "type": "existing" leaves (older heading cards are "nodeType": "text"), an origin at the board's top-left and "placement": "exact". Run it with --dry-run first and check that the bbox bottom stays above anything below the bands, such as a section.

Fan-out under the hero and banner (the failure case the agent kept getting wrong). Every new canvas opens this way: the large hero first, then the medium banner with no heading of its own, then the board:

{
  "root": {
    "kind": "stack", "axis": "vertical", "gap": "medium",
    "items": [
      { "kind": "item", "type": "richtext", "size": "large",
        "content": "# Four goals we will hit by March" },
      { "kind": "item", "type": "richtext", "size": "medium",
        "content": "On 3 Jan 2026 the team agreed four goals for Q1.\nThis board is what we commit to and who owns each goal.\nAna reviews progress weekly on each goal card.\nRead left to right." },
      { "kind": "grid", "columns": 4,
        "items": [
          { "kind": "item", "type": "note", "noteId": "GOAL-1" },
          { "kind": "item", "type": "note", "noteId": "GOAL-2" },
          { "kind": "item", "type": "note", "noteId": "GOAL-3" },
          { "kind": "item", "type": "note", "noteId": "GOAL-4" }
        ] }
    ]
  }
}

SWOT-style four-quadrant layout:

{
  "root": {
    "kind": "stack", "axis": "vertical", "gap": "spacious",
    "items": [
      { "kind": "stack", "axis": "horizontal", "gap": "spacious", "items": [
        { "kind": "grid", "columns": 2, "items": [
          { "kind": "item", "type": "note", "noteId": "STR-1" },
          { "kind": "item", "type": "note", "noteId": "STR-2" }
        ] },
        { "kind": "grid", "columns": 2, "items": [
          { "kind": "item", "type": "note", "noteId": "WEAK-1" }
        ] }
      ] },
      { "kind": "stack", "axis": "horizontal", "gap": "spacious", "items": [
        { "kind": "grid", "columns": 2, "items": [
          { "kind": "item", "type": "note", "noteId": "OPP-1" }
        ] },
        { "kind": "grid", "columns": 2, "items": [
          { "kind": "item", "type": "note", "noteId": "THR-1" }
        ] }
      ] }
    ]
  }
}

Anchor a new column to the right of an existing note:

{
  "root": {
    "kind": "anchor", "to": "NOTE-12", "direction": "right", "gap": "medium",
    "child": {
      "kind": "stack", "axis": "vertical", "gap": "tight",
      "items": [
        { "kind": "item", "type": "note", "noteId": "NEW-1" },
        { "kind": "item", "type": "note", "noteId": "NEW-2" }
      ]
    }
  }
}

Diamond topology with edges:

{
  "root": {
    "kind": "stack", "axis": "vertical", "gap": "medium", "align": "center",
    "items": [
      { "kind": "item", "type": "note", "noteId": "A", "key": "root" },
      { "kind": "stack", "axis": "horizontal", "gap": "medium", "items": [
        { "kind": "item", "type": "note", "noteId": "B", "key": "left" },
        { "kind": "item", "type": "note", "noteId": "C", "key": "right" }
      ] },
      { "kind": "item", "type": "note", "noteId": "D", "key": "tail" }
    ]
  },
  "edges": [
    { "from": "@root", "to": "@left" },
    { "from": "@root", "to": "@right" },
    { "from": "@left", "to": "@tail" },
    { "from": "@right", "to": "@tail" }
  ]
}

Workflow

  1. Write the layout doc to a temp file (/tmp/layout.json).
  2. Run cnotes canvas place <canvasId> --spec /tmp/layout.json --json.
  3. The response includes items[] (with resolved positionX/Y, width, height, id, and your key), edges[] (with resolved edge ids), and bbox (the bounding box of the whole layout).

Constraints

  • Anchors take zero space in their parent — when nested inside a stack or grid, an anchor reserves no slot, so siblings collapse together. Anchors are best used at the root or as their own top-level branch.
  • Anchor to: must be a node already on the canvas — local @key refs only work for edges, not for anchor targets in this version.
  • Never use ALL CAPS in a text element — headings included. Use title case (e.g., "Key Tensions" not "KEY TENSIONS").
  • Text-element content is authored as markdown — the server converts it server-side. A heading is "# Key Tensions" with no colorHex. Keep it card-sized (canvas-elements.md, hard rule 3).

When the layout engine isn't enough

A few cases still need explicit x/y via bulk-add:

  • Reproducing a canvas pixel-for-pixel for a demo/screenshot
  • Templates with intentional off-grid placement

The single-item adds (add-node, add-text, add-list, add-link, add-section) go through the solver when you omit --x/--y, and into a section with --section. Passing --x/--y is the unchecked escape hatch. In all other cases, and especially when adding 3+ items at once, use place.

Canvas commands

Read this for the canvas commands themselves: reading, creating, adding and editing elements, moving and removing, edges, templates, milestones and the digest. Which element to use is in canvas-elements.md; placing many items is in canvas-layout.md.

Read, create and edit

# List / get (archived canvases excluded by default)
cnotes canvas list [--include-archived]
# Find a canvas by name, topic, or MEANING — semantic similarity over canvas-digest
# embeddings blended with lexical match (name / themes / digest), so a query sharing no
# words with a canvas can still surface it. This is the canvas-level counterpart to
# `cnotes search semantic` (which searches NOTE content); they are separate corpora, so to
# cover both you search each. Pass '*' or 'all' to enumerate every canvas. JSON results
# carry id, displayId, title, matchedBy (lexical|semantic|both), relevance (0..1),
# purpose, themes, contains, noteCount, snippet, and `open` (deep link).
cnotes canvas search "<query>" [--limit <n>]
cnotes canvas get <canvasId>
# `get` returns each node kind in its own array (nodes/textNodes/richtextNodes/
# listNodes/canvasLinkNodes) AND a unified `allNodes` array — every node flattened
# to {id, kind, displayId, label, positionX, positionY, width, height} — plus
# `contentBounds` (the bounding box of everything). Use allNodes/contentBounds to
# reason about occupancy in one read instead of unioning the per-kind arrays.
# Portals live in canvasLinkNodes, NOT nodes: checking only `nodes` is how a card
# got dropped exactly on a portal. (You rarely need this for placement —
# `cnotes canvas place` already avoids overlaps.)
# See the whole layout before touching x/y: every item's box by kind, the pairs
# that overlap, and with --near a free spot next to an anchor.
cnotes canvas status <canvasId> [--near <displayId|nodeId|x,y>] [--size 340x130]

# Read a canvas as STRUCTURE, not just content — one round-trip to think across it.
# Returns the notes as concatenated markdown in display order (top-to-bottom, then
# left-to-right) GROUPED UNDER THEIR SECTION FRAME, plus the edges (the argument)
# and the portals (the navigation). --json gives { canvas, sections, elements,
# edges, portals }; every element carries the `section` it sits inside.
# You should not need a second `canvas get` just to see the frames or the wiring.
cnotes canvas read <canvasId>
# A share link someone sent (https://…/shared/<id>) reads directly, no workspace
# needed: same access the link gives this account in a browser. A 404 means it
# isn't shared with this account (or doesn't exist); ask the owner to share it.
cnotes canvas read https://creatornotes.app/shared/<canvasId>

# Read ONE section frame. Takes the SECTION-<n> handle ALONE — the canvas is resolved
# for you — so this is the right call whenever the user names a frame ("look at
# SECTION-6GC", "what's in the risks section") instead of reading a whole canvas and
# filtering it by eye. Returns the notes, stickies, lists and portals whose CENTER sits
# inside the frame, any frame nested within it (under its own heading), plus the edges it
# touches (an edge leaving the frame is marked with where it goes).
# --json gives { canvas, section, nestedSections, elements, edges, portals }.
cnotes canvas read-section <SECTION-n>

# AI digest of canvas: narrative summary + per-note summary list for every note on the canvas
cnotes canvas digest <canvasId>

# Create / update / delete
# NAMING: separate parts with the interpunct · (U+00B7, MIDDLE DOT, not a bullet •),
# never an em or en dash: "Path to Revenue · Funding strategy".
cnotes canvas create "<name>" --goal "<text>" [--audience "<text>"]   # prints Ref (CANVAS-N) + Link; end your reply with them
cnotes canvas create "<name>" --goal "<text>" --audience "<text>" --spec spec.json --intent "<headline>"
#   the canvas AND its first layout in one call: --spec is the `canvas place` document, notes included;
#   the placement comes back under `layout`. A refused layout creates nothing: fix the spec, run it again.
# --goal is not optional in practice. Write it FIRST, for a reader, in two short sentences:
#   <Decide | Rank | Agree | Show | Plan | Record> <the object>. Done when <what is visibly true on the board>.
#   e.g. "Decide which phone-section bugs ship in the next PR. Done when every card is in a PR or marked won't fix."
# The first sentence is the result the board produces; the second is the verifier, so a reader (or
# the digest) can tell finished from not. The server REJECTS a goal with no "Done when" sentence or
# over 60 words. Leave out: the list of sections (the frames show it), the method ("research
# Tufte"), dates (banner line 1) and the audience (--audience). "Track X" / "Explore X" are briefs
# to yourself, not results; give them an exit ("Decide which X ship next. Done when ...").
# `canvas read` prints the goal under the title, so re-read it before adding to a board and keep
# the board inside it; fix a stale goal with `canvas update --goal`. The banner restates the goal
# in reader's words (canvas-elements.md, hard rule 7).
cnotes canvas update <canvasId> [--title "<text>"] [--goal "<text>"] [--audience "<text>"]
# Is the board done? The judge's last reading of whether the goal's "Done when" is true of
# the board: met / partly met / not met, the deciding cards and one sentence why. Derived,
# never a status anyone sets. --refresh judges now (one model call, editors only; refused
# when the deployment has scoring off). Where a decision model is configured, the Done when
# is split once into its parts ("and" separates parts, "or" stays one part), the split is
# reused until the sentence is reworded, and the verdict lists each part as met or not yet
# with who ruled (code, the decision model, the language model). A part that names a note
# type ("a Spec note") needs a card of that type ON the board. Goal trees (`cnotes goals
# tree`, goal-tree skill) are built from these.
cnotes canvas done <canvasId> [--refresh]
# Which cards contradict each other, and which claims nothing on the board backs. Writes
# nothing and gives no reasons, so read each finding before acting on it. For a real one,
# open a draft (`cnotes draft open --from-canvas <canvasId>`) and write a Tension note that
# mentions both cards, or a Question note asking what would back the claim; the person
# accepts the draft. "Unsure" findings are for the person, not for a note. One model call
# per card; editors only; refused where the deployment has no decision model for the workspace.
cnotes canvas check <canvasId>
cnotes canvas delete <canvasId>
cnotes canvas set-as-home <canvasId>
cnotes canvas archive <canvasId>
cnotes canvas unarchive <canvasId>

# Add nodes. Which element to use: canvas-elements.md. To put several cards on a canvas,
# use `canvas place` (canvas-layout.md): it packs them overlap-free. The single adds below
# go through the same solver when you omit --x/--y (clear of everything), and INTO a
# section with --section SECTION-<n> (the section arranges itself around the new card).
# --x/--y is the raw escape hatch: no collision check, it only warns after the fact.
cnotes canvas add-node <canvasId> --note <noteId> [--section SECTION-<n>]   # NOTE nodes only
# text = free-form markdown on the canvas (wire type: richtext): headings, emphasis,
# images, and `[NOTE-12: Title](relationship:verb)` mentions, which render as chips.
# No background unless you pass --color. A heading is `--content '# Q1 Goals'`.
# --size sets the type as well as the width (headings 30/56/128px). Every canvas opens with a
# `large` untinted hero, `# <the goal's result>` under 50 chars (canvas-elements.md rule 7).
cnotes canvas add-text <canvasId> --content "<markdown>" [--size small|medium|large] [--color "<hex>"] [--section SECTION-<n>]  # inline (short only)
cnotes canvas add-text <canvasId> --content-file <path.md> [--size small|medium|large] [--color "<hex>"] [--section SECTION-<n>]
cnotes canvas add-richtext ...   # DEPRECATED alias of add-text (identical flags). Use add-text.
# THE BOX IS DERIVED, NOT CHOSEN. If you pick x/y, the server still picks the size. WIDTH is the
# --size preset: small 560px, medium 1120px, large 1680px. HEIGHT follows the content,
# including after `canvas update-text` changes the copy. So both halves bite: columns
# pitched for a small card while the items are medium overlap by 460px each, and longer
# copy at the same x/y runs over whatever sits below. The write still succeeds either way.
# Longer copy is handled for you: when the card reaches DOWN into what starts below its top
# edge (notes, lists, section frames and their title chips), the server shifts that whole
# band down to clear it and says so (ripple {delta, movedNodeCount}; CLI prints "Made room"
# on stderr). Re-read positions with canvas get before placing next to anything it moved.
# What it cannot move comes back as layoutWarning (CLI also prints it to stderr): a column
# BESIDE the card, or siblings in the frame the card sits inside. Reflow those (bulk-move /
# arrange), or hand the block to `canvas place`, then re-check. Never treat "write succeeded"
# as "layout is fine" after a size bump or a content change.
# A list is a collection of member notes (--notes); --description is a one-line frame.
# N distinct things = N notes first, then --notes ID1,ID2,ID3 (canvas-elements.md).
cnotes canvas add-list <canvasId> --description "<markdown>" [--notes <id1,id2,...>] [--view list|grid] [--section SECTION-<n>]
cnotes canvas add-link <canvasId> --target <otherCanvasId> [--section SECTION-<n>]
# quote = the exact words from one note, as a Quote note (QUOTE-<n>) pinned to the note's
# SAVED version, placed as a card drawn as a quote. Evidence, not a claim: the words
# never change, and the card says "now vN" once the source moves on. The same words
# from the same version are ONE Quote note: quoting them on another canvas places
# that QUOTE-<n> again (or `canvas add-node --note QUOTE-<n>`), and search finds it
# by its words. Lands beside the source note's card, clear of other cards, else
# below everything; --x/--y override. --title is your own words for why the line
# matters (default: the quoted words). The words must appear in the saved version
# (exit 3 otherwise). Say what a quote means in an Insight that cites QUOTE-<n>.
# --size small|medium|large and --color <role> (evidence, commitment, insight, warning,
# danger, people) set hierarchy and meaning; canvas-elements.md rule 8 says when.
cnotes canvas add-quote <canvasId> --note <TRANSCRIPT-4> --text "<exact words>" [--style pull|highlight|evidence] [--size small|medium|large] [--color <role>] [--title "<why it matters>"]
# Restyle a Quote note's card already on the board (its box resizes with it).
cnotes canvas style-quote <canvasId> --quote QUOTE-<n> [--style ...] [--size ...] [--color <role>|none]
# section = a NAMED REGION frame (Miro-style). Renders behind content; dragging the frame
# carries whatever sits inside it. Mints SECTION-<n>: mention [[SECTION-<n>]] from a note
# body to deep-link readers to that region. Name like a place ("Shaping", "Open Questions").
# Returns the displayId — surface it. Frames are NOT collision obstacles: content is placed
# INTO them (canvas get returns sectionNodes with bounds).
# PREFERRED: --notes creates the frame WITH those notes laid out inside it in one call
# (grid, frame sizes itself around them, collision-aware — no coordinate math). Such a
# section is ARRANGED: it re-lays itself out as cards are added, edited or removed, until
# a card is moved by hand (canvas-layout.md, "Arranged sections").
cnotes canvas add-section <canvasId> --name "<title>" --notes <id1,id2,...> [--columns <n>] [--color "<hex>"]
cnotes canvas add-section <canvasId> --name "<title>" [--width <n>] [--height <n>]   # empty frame, solver-placed

# Resize / rename / move / recolor an EXISTING frame, IN PLACE. Takes the SECTION-<n>
# display id. ALWAYS use this to grow a frame around new content — deleting and
# re-adding a section mints a NEW id (SECTION-51 becomes SECTION-57) and silently
# breaks every [[SECTION-51]] mention pointing at the old one.
cnotes canvas update-section <canvasId> --section SECTION-51 --height 1800
cnotes canvas update-section <canvasId> --section SECTION-51 --name "Open Questions" [--x <n>] [--y <n>] [--width <n>] [--color "<hex>"] [--clear-color]
# Edit a Text card's words IN PLACE (same node id, edges and mentions survive). This is the
# only way to change a card's copy; never delete and re-add it.
# Card ids: the `id` on kind:richtext elements in `cnotes canvas read <canvasId> --json`.
# --find must match exactly once.
cnotes canvas update-text <canvasId> --node <nodeId> --find "picks none" --replace "picks none (10 Oct 2026)"
cnotes canvas update-text <canvasId> --node <nodeId> --append "Answered on 10 Oct 2026."
cnotes canvas update-text <canvasId> --node <nodeId> --content-file ./banner.md [--size large] [--color "<hex>"]

# Place items using a declarative layout (NO x/y math — server packs them)
# This is the PREFERRED way to add multiple items. Spec and examples: canvas-layout.md.
cnotes canvas place <canvasId> --spec ./layout.json
cnotes canvas place <canvasId> --spec-stdin                  # JSON from stdin
cnotes canvas place <canvasId> --spec-inline '<short JSON>'  # for tiny specs only

# Tidy a section: lay out what is already inside it, size the frame to fit, keep it arranged.
# The fix for a section whose cards overlap or spill past the frame (canvas status lists both).
cnotes canvas arrange <canvasId> --section SECTION-90
cnotes canvas arrange <canvasId> --section SECTION-90 --stop        # back to freeform, nothing moves
# Put more cards into a section: appended to its layout, the frame re-arranges
cnotes canvas place <canvasId> --spec ./cards.json --into SECTION-90

# Reflow EXISTING nodes (mixed kinds) into a section — preferred over bulk-move + update-section
cnotes canvas arrange <canvasId> --section SECTION-90 \
  --nodes '[{"nodeId":"<id>","nodeType":"canvas"},{"nodeId":"NOTE-3","nodeType":"note"}]' \
  --layout grid --columns 4 --gap medium

# Bulk add with explicit positions (escape hatch — only for pixel-perfect templates)
cnotes canvas bulk-add <canvasId> --notes '[{"noteId":"NOTE-1","x":100,"y":100},{"noteId":"NOTE-2","x":400,"y":100}]'

# Move / remove nodes (--type for non-note nodes, e.g. --type section)
# move-node is LOW-LEVEL: raw x/y, no collision check (it warns when the node lands
# on something). To tidy or reorganize, use `canvas arrange`; to pick a spot by
# hand, get one from `canvas status <canvasId> --near <ref>` first.
# NOTE: `text` in these type lists is the LEGACY heading kind (old canvases only). It can no
# longer be CREATED, but existing ones stay movable and deletable — that is why it is still
# accepted here. Today's Text element is `richtext`.
cnotes canvas move-node <canvasId> --node <nodeId> --x <n> --y <n> [--type note|richtext|list|canvas|section|text]
# --node takes a node id, a SECTION-<n>, or a note display id naming that note's card here.
cnotes canvas remove-node <canvasId> --node <ref>

# Bulk remove — the way to REVISE a canvas instead of only adding to it.
# --nodes entries may be a node id, a SECTION-<n>, or a note display id naming that
# note's card on THIS canvas. Edges on a removed node come off automatically; pass
# --edges only to drop an edge while keeping both of its nodes.
cnotes canvas bulk-remove <canvasId> --nodes '["SECTION-7","DECISION-3"]' [--edges '["edgeId1"]']
# Removal is PLACEMENT-ONLY: the note keeps its content, versions and display id and
# stays on every other canvas. It is not archiving — and archiving a note does NOT take
# it off a canvas, so a superseded card has to be removed here as well as archived.
# Run it inside an operation (or `canvas agent-run wrap`) so the removal is undoable as a unit.

# Bulk move multiple nodes at once (more efficient than repeated move-node)
cnotes canvas bulk-move <canvasId> --moves '[{"nodeId":"abc","nodeType":"note","x":200,"y":300},{"nodeId":"def","nodeType":"richtext","x":500,"y":300}]'
# nodeType: note | richtext | list | canvas | section | text (defaults to "note"; `text` = legacy)

# Connect any two nodes on the same canvas. --source/--target take a node id, a SECTION-<n>
# (the edge anchors to the frame's border), or a note display id for its card here.
# Labels are short verbs ("fixes", "grounds", "then"). Edges are canvas-local: mirror a
# note-to-note edge as a mention in the note body (canvas-elements.md, hard rule 5).
cnotes canvas add-edge <canvasId> --source <ref> --target <ref> [--label "<text>"]

Templates

# Templates — create structured canvases from predefined layouts
cnotes canvas templates                                           # List available templates
cnotes canvas from-template <templateId> [--title "<title>"]      # Create empty skeleton
cnotes canvas from-template <templateId> --populate               # Auto-populate zones with matching notes
# Templates: sprint-retrospective, swot-analysis, kanban-board, feature-prioritization, meeting-notes, product-roadmap

Milestones

# Milestones — the same canvasSnapshots rows the web UI saves (kind "milestone").
# The message is one field: the first line is the timeline headline, a blank
# line, then the body. The headline MUST start with a past-tense verb and name a
# specific object in the first 40 characters ("Shipped the public comic canvas",
# never "V1"); the server rejects anything else with exit code 3 and saves
# nothing. Headline at most 110 characters. The platform may tidy an agent
# headline and leaves the body. `headlineWarnings` comes back on the JSON response.
# See https://creatornotes.app/blog/timeline-as-navigation
# An AI summary is stored separately and does not replace this message.
# If an operations run is active, the save attaches to it and records that run's
# actor, so an agent milestone is not labelled as a human edit.
cnotes canvas milestone create <canvasId> --message "Opened pricing question: charge for agents or the workspace?"
cnotes canvas milestone create <canvasId> --message-file ./milestone.txt
cnotes canvas milestone list <canvasId>
cnotes canvas milestone list <canvasId> --json
cnotes canvas milestone show <milestoneId>
cnotes canvas milestone rename <milestoneId> -m "Shipped the public comic canvas"  # new headline, body kept

Agents creating or editing canvas milestones (cnotes canvas milestone create / rename, the REST routes, or the web save dialog) must use a headline that starts with a past-tense verb and names a specific object, e.g. Shipped the public comic canvas, never V1. The rule is enforced: a milestone create or rename whose headline is not verb-first, or names no object, is rejected (exit code 3) and nothing is saved. Read the error, rewrite the first line, and retry. To fix a milestone that is already saved, run cnotes canvas milestone rename <milestoneId> -m "<headline>"; it replaces the headline and keeps the body.

  • Good: Shipped the public comic canvas, Narrowed pricing to per-seat, Dropped the onboarding survey
  • Rejected: V1, Launch prep, The board after review, Shipped

Canvas Orientation

On a large canvas, orient with cnotes canvas digest <canvasId> --json before reading note by note. The digest returns the narrative summary, key themes, and a per-note summary list covering every note on the canvas. Use canvas read when you need the structure (frames, edges, portals) or are about to change the canvas.

--json field names (they aren't what you'd guess — the narrative is .digest, not .narrative or .summary). Top-level shape: { status, canvasName, digest, themes[], notes[], noteCount, generatedAt }. So --fields digest gets the narrative, .canvasName the title, .themes[] the key themes, and .notes[] the per-note list (shape below).

The notes array entries have the shape { displayId, title, type, summary }. When summary is null, the AI summary hasn't been generated yet (fresh note, or too short to summarize); run cnotes notes get <displayId> [<displayId>...] (batch-fetch in one call) if you need full content.

If status is "none" or "stale", the notes array is still populated and accurate — you can start working immediately. Queue a fresh narrative with cnotes canvas digest <canvasId> --refresh (a write; needs Pro) only if you need the narrative itself.

Drafts

Read this before reworking notes or a board other people read, or whenever you are asked to draft something.

What a draft is

A draft is a private workspace opened FROM the active one. Only the person who opened it, and agents using their login, can see it. Nothing in it reaches the shared workspace until the person asked approves it: in the browser, or, for a proposed version of a board, through their own agent (cnotes draft accept under their key). The key that built a draft can never land it. You build the draft; the person asked decides it.

You work IN a draft; it never becomes your workspace. draft open and draft checkout leave the active workspace as it is and send every command aimed at it into the draft, whether the workspace came from the saved config, CNOTES_WORKSPACE or -w. cnotes workspace current names the shared workspace and the draft ("Working in: PROPOSAL-9 · ..."). cnotes draft leave works live again; the draft stays open.

In place

Where a workspace has it on, draft open without --from-canvas opens a proposal in place instead: no draft workspace is created. draft open says "Opened PROPOSAL-16 ... in place". While you work in it, commands stay aimed at the shared workspace and the CLI sends the proposal with each one: new notes and canvases are rows of the shared workspace that only you and the person you ask can see, and saving a live note (versions create or draft propose) proposes a rewording. A change to live work is refused with live_write_from_proposal (a card or edge on a live board, a live board's name, archiving or retyping a live note): build on the proposal's own canvases, and say the rest in --left-out. draft status, ask, accept, send-back, dismiss, discard and leave work the same way. Branching a board (--from-canvas) still opens a draft workspace.

When to open one

Open a draft before you:

  • rework notes other people read (a guide, a decision others cite);
  • rework a board other people read (branch it, below);
  • write many interlinked notes at once;
  • do anything you were asked to "draft";
  • give review feedback on shared work when you can write the fix (reviews-and-comments.md).

Branching a board

To rework a board other people read, branch it: cnotes draft open --from-canvas CANVAS-390. The draft holds your working copy (it prints its id, say CANVAS-412) and the live board does not change. Inside the draft, the ordinary canvas verbs work on CANVAS-412: canvas read, place (shared notes included), move-node / bulk-move / arrange, remove-node / bulk-remove, add-edge. A canvas write aimed at CANVAS-390 while you work in the draft is refused and names your copy. A person accepts your copy as the board's next version (V2), on the live board.

Commands

cnotes draft open "Pricing rethink" --intent "Teams of five pay the solo price and churn at renewal"   # opens it; you now work in it
cnotes draft open --from-canvas CANVAS-390 --intent "Screens 4 to 7 still show the old stamp" --answers QUESTION-12   # branch a board, say why
cnotes notes create --notes '[...]'      # lands in the draft (you work in it)
cnotes notes get DECISION-12             # a SHARED note reads through from the draft (sharedFrom in --json)
cnotes draft propose DECISION-12 --markdown-file new.md --description "Annual is the default"
cnotes draft edits                       # the proposals: proposed title, base version, conflict marker
cnotes draft notes                       # the notes and canvases it adds (what the person asked reads)
cnotes draft discard DECISION-12         # withdraw one; the shared note was never touched
cnotes draft status                      # who it waits on, what V2 changes, the checks, the link
cnotes operations end --unsure "Kept the cap in Open; it could be its own Decision"   # flag a judgment call, in the run that made it
cnotes draft ask --reviewer sam@example.com --headline "Resolved the sign-in ticket with its Decision" --changes "<what is true once they accept>" --left-out "<what they will look for and not find>"   # say what it is; then STOP
cnotes draft list                        # your open drafts across workspaces (--all adds closed)
cnotes draft checkout PROPOSAL-9         # resume in a NEW session: handle, id or unique name substring
cnotes draft leave                       # work live again; the draft stays open
cnotes draft dismiss                     # close it; nothing reaches the shared workspace

Naming a proposal and its new notes

Every draft has a handle, PROPOSAL-78, printed by draft open, draft list and draft status and shown in the person's Proposals panel. The person may say "proposal 78" or "PROPOSAL-78" in chat: every draft command takes it (cnotes draft checkout PROPOSAL-78, draft status proposal 78). Handles are numbered per shared workspace, so a handle names a proposal of the active workspace (the shared one, also while you work in one of its drafts) only. When it is not there, the command refuses and names each workspace that has one, with the -w <workspaceId> that selects it. Notes written inside a draft have their display id from the start (DECISION-6K12) and keep it when the draft lands, so they can be named and linked before then: [DECISION-6K12: Title](relationship:answers) from another note in the draft works. From the shared workspace they are not there yet; cnotes notes get DECISION-6K12 says which proposal holds one and the checkout that reaches it.

The loop

  1. Say why when you open it. --intent is the problem the draft answers, in the person's words, never the task: "Reviewers read IBD and controlled money as two things", not "Add a glossary". The person reads it as Why at the top of the review, and the server refuses one that opens with a verb (exit code 3, nothing saved). --answers QUESTION-12 names the shared note that asked (the Question, the Problem, the note holding the request). Both also open draft status when a later session checks the draft out, so the work keeps its direction.

  2. Build inside the draft, one run per change. New notes and canvases land in it, and a branch copy takes your canvas edits. The person reviews your version run by run, each under the intent you began it with (cnotes operations begin --prompt "Resolve the sign-in ticket with its Decision"), the way a pull request is read commit by commit. So bracket one intention per run, named as the change, and never a run whose prompt lists three things with "and". Reflowing cards inside a frame is folded away as layout; moving a card into another frame, adding, removing, relinking and rewording are what the person reads. Link to shared notes normally: [DECISION-12: Title](relationship:builds-on) resolves to the shared note and stays a working link once the draft lands. When a run made a call you were not sure of, say so as you end it: cnotes operations end --unsure "<the call, and the alternative>". Write it then, while the doubt is real; it cannot be reconstructed later. The person sees those runs first.

  3. Change a shared note by proposing it, never live. Read it (cnotes notes get DECISION-12 works from inside the draft), write the complete new body, then cnotes draft propose DECISION-12 --markdown-file new.md --description "<what changed>". An ordinary cnotes versions create DECISION-12 ... while you work in the draft does the same: it answers "Proposed a rewording of DECISION-12 in PROPOSAL-9" (proposed: true in --json) and writes nothing live. The shared note keeps its text until the draft lands. Proposing the same note again replaces the proposal. Archive, retype, move and relationship deletes on a shared note are refused from inside a draft; say them in --left-out instead.

  4. Run cnotes draft status until no check blocks. Blocking: a link to a shared note that is archived, a citation of a shared note that moved since you cited it (stale-citation, in a draft note or in a proposed new text: read it again and save the citing note, or propose the text, again), and a proposal whose base moved (base-conflict: read the current text, fold it in, propose again). Warnings never block: a missing title, an empty body, stale-read. Every finding names the card and the fix. For a branch, status also prints "V2 of CANVAS-390: 2 added, 5 moved, ...", each conflict with the live board (a card changed on both sides; make your copy match the live board's value to clear it), and how many cards changed on the live board since you branched.

  5. Say what it is, ask, then stop. On a branched board, cnotes draft ask --reviewer <their email> names the person who decides (an editor of the shared workspace, usually the board's owner). Its message is what they read before the runs, so each line answers one question and none repeats another:

    • --headline: what it did. Past-tense verb, the specific object, under 110 characters; on accept it becomes the board's milestone.
    • --changes: what is true once they accept that is not true now, the claim they agree to. Never the list of cards or a count (the runs show every card), never the headline again.
    • --left-out: what they might look for and will not find, so they neither review for it nor assume it is done.

    Why is the --intent from step 1. A sentence or two each. Cover the headline and read the rest: a line that adds nothing to it is cut. The server enforces this the way it enforces milestone headlines: a Changes or Left out line that lists items, counts cards, opens with a past-tense verb (Changes) or repeats the headline or the Why is refused with exit code 3 and nothing is saved. Read the error, rewrite that line, ask again.

    Flat, three lines that list the same thirteen terms:

    Added a thirteen-Term glossary that fixes one word per trust accounting concept Why: Add a glossary of Term notes so the workspace uses one word per concept: IBD, controlled money, controlled client bank account, ... each saying which words it replaces Changes: Thirteen Term notes on one Glossary board: IBD, controlled money, controlled client bank account, ...

    Sharp, each line new:

    Added a thirteen-Term glossary that fixes one word per trust accounting concept Why: The board calls the same money IBD, controlled money and client funds, and reviewers read them as three things. Changes: Every concept has one name and each Term says which words it replaces, so a note still using an old word reads as out of date. Left out: The notes that use the old words keep them; rewording them is the next draft.

    This is the same shape as any ask (reviews-and-comments.md): the headline says what this is, the changes say what it decides, and left-out says what you are not asking about. Each run is an item the person accepts or skips, so its prompt is the claim they confirm. On a draft of notes (no branched board), --reviewer me asks the person whose login you use, who approves it in the draft; naming a teammate works the same as for a board, and they review it from their Proposals panel. It prints the link they open; post it, then stop. Never try to merge or approve what you built: cnotes draft merge and cnotes review complete --outcome approved on your own draft are refused, and pressing Accept or Approve through a browser tool is not yours to do either. If a check blocks when you ask, the ask is still made, each check is printed on stderr, and the person sees it blocked until you fix it and ask again.

  6. When they send it back, cnotes draft status opens with their line ("Changes requested by <name>: ..."). Fix it in the draft, run status until no check blocks, then draft ask again and stop.

  7. When the live work overtakes it, both people are told. A shared note it rewords or cites changed, or a card changed on the live board and in your copy: draft status and draft list open with it ("DECISION-12 "Pricing" changed live after you asked."), then the fix. The person asked reads "Waiting on <you>: ..." instead of a proposal to decide. Make the fix, then draft ask again and stop. A live save of a note with a proposed new text prints that the save puts the proposal out of date.

  8. When another proposal rewords the same note, you are told wherever you look: draft ask prints "Overlaps: PROPOSAL-7 "Team price" also rewords DECISION-1, built by you via Codex (Slack)." on stderr, draft status lists it as a proposal-overlap warning, and operations begin lists it under the proposals waiting on you. You only ever hear of a proposal you could already see (yours, or one asked of you). It never blocks. Read the other one (cnotes draft status PROPOSAL-7) and decide which of three holds: they agree (say in your ask which should land first), yours repeats it (fold what is new into the other or dismiss yours), or they conflict (write a Question for the person naming both, with your lean).

Deciding as someone's agent

When the person whose login you use was asked to decide a proposed version of their board, cnotes asks lists it as decide with the draft id. Read it with cnotes draft status <draftId> (V2 against the live board, each conflict, the checks, and under each run the cards it added, moved into another frame or removed, by display id and title; --json puts them in proposal.runs[].cards) and the reworded notes with cnotes draft edits --draft <draftId> --note <NOTE-n>. Then cnotes draft accept <draftId> --note "why" lands it as the board's next version, as them, or cnotes draft send-back <draftId> --note "what should change" returns one line to the proposer. Accept re-reads everything first and refuses on a blocking check or a conflict. History shows the landing as "approved by <them>, through their agent", and Undo stays theirs in the browser. Decide only when the person told you how; when unsure, say what you read and let them click. A decide row that "waits on <proposer>" was overtaken by the live work: there is nothing to accept until they bring it up to date.

A draft of notes, with no board, is listed the same way. Read its new notes with cnotes draft notes <draftId> (and one with --note <NOTE-n>) and its rewordings with cnotes draft edits; cnotes draft accept <draftId> lands it whole.

To take only part of a board's version, --runs 1,2,5 (the run numbers in draft status) lands those runs and leaves the rest proposed in the draft. Runs marked "goes with" change the same cards, so they are named together or not at all. An accept that takes a run its agent was unsure of says so on stderr, with the --runs that leaves it out; it never refuses on that alone. As the builder, a status line reading "Merged runs 2 and 4 of 4. Runs 1 and 3 are still proposed." means the rest is yours again: fix it and ask, or dismiss it.

Preparing the answer for your own person

When another agent of the same person built a proposal in place and asked that person, you cannot accept it: no key of theirs lands their own agents' work. You can prepare the answer they give. operations begin lists it under waiting on you, with "built by you via Codex (Slack)". Read it with cnotes draft status PROPOSAL-n and its rewordings, check it against what it rewords, then:

cnotes draft prepare PROPOSAL-n --accept --note "<why it should land>"
cnotes draft prepare PROPOSAL-n --send-back --note "<what should change>"

The person reads "<your key's name> recommends landing it" with your line on the review, in the browser or on the phone, and gives the answer with one tap. Then stop. You need your own key (cnotes auth keys create --name "<agent>"): the key that opened, wrote into or asked for the proposal is refused, and two agents on one login key count as one. An accept is refused while a check blocks; prepare a send-back that says what to fix. A later write into the proposal clears what you prepared, and operations begin says "<agent> prepared: accept" so a second reviewer does not start again.

What to rely on

  • One line says who it waits on. draft status, draft checkout and workspace current print it: nobody asked yet, the changes a person asked for, or a named reviewer. When the next move is yours, it ends with the draft ask to run. --json carries it as decision.line, with decision.state (awaiting_person, changes_requested, waiting_on_reviewer).
  • Ids survive. A draft mints display ids from the shared workspace's sequence, so DECISION-6K9 written in a draft is still DECISION-6K9 once it lands. In the rare clash the id is re-minted, every mention is rewritten, and draft status lists the change (merge.renamed in --json).
  • The draft you work in is saved for this login on this machine. Every terminal using it works in the draft too. When two agents share a login, pin each terminal: CNOTES_PROPOSAL=<draft id> works in that draft and CNOTES_PROPOSAL=none works live, whatever is saved. The id may be a proposal this machine never saved (a second agent handed only the id); the CLI asks the server whether it is a proposal in place before the command runs. A command aimed at another workspace (-w, CNOTES_WORKSPACE) works there, live; -w naming the draft's own shared workspace still goes into the draft. Type setup is live work: types update refuses inside a draft, because a merge carries notes, never type changes.
  • After it lands, draft status says who approved it and which ids were renamed, and the CLI stops sending commands into it (so does workspace current): you work live in the shared workspace again. The person who approved it can undo the merge while nobody has built on it; a key never can. Hand them the link on the Undo: line (undoUrl in --json).
  • When the live board moved under a branch, start over rather than merge. Settle one or two conflicts on the copy as draft status says. When the board changed under much of your work, or a conflict needs the board's owner to act, run cnotes draft rebranch: the copy is copied again from the live board as it stands (no conflicts, today's base), nothing of your board work is kept, and it lists what V2 had done under each run's intent. Redo what still makes sense on the board as it is now, one run per intent, then draft status and ask again. Your draft's notes and proposed rewordings stay; their cards on the copy go, so place them again. The list stays in draft status until the next re-branch.
  • Canvases in a draft can hold shared notes. Place them as usual; the card points at the shared note, and to change what it says, propose it.
  • It lands all at once: at most 60 notes, 25 canvases and 100 proposals. Only live items count: archived notes and canvases stay in the draft, and any link, card or list item pointing at them is dropped so their titles never reach the shared workspace. The draft's empty starting canvas stays behind.
  • A draft can't be shared. Invites, email shares and public links refuse in a draft. The hand-over is draft ask, nothing else: for a branch, it is what lets the person you name see your version at all.
  • Wrap the work in an operation as usual (operations begin while you work in the draft): runs attribute to the draft, and follow the work onto the shared timeline when it lands.

The CreatorNotes humanizer

Load this before you draft, audit, or rewrite audience-facing prose that will be saved to CreatorNotes: pre-reads, workshop notes, strategy notes, public drafts, Slack messages, canvas orientation banners, and the prose body of typed notes. Its job is to remove generic model habits so text lands as a real human author wrote it, without corrupting meaning or erasing a legitimate voice.

Two things this is NOT:

  • It is not a truth checker. The model cannot verify a claim is correct; it can only flag the three defects listed under The three verifiable flags.
  • It is not a personal-voice generator. Removing slop creates a quality floor. A person's voice is a separate, optional layer (see Two layers).

First decide: does this text get touched at all?

Classify the task before editing a single word. Getting this gate right matters more than any word-level fix, because the failure that hurts is rewriting something that should have been left alone.

Apply the humanizer automatically — agent-generated prose meant for a reader: pre-reads, Slack messages, strategy prose, workshop notes, public drafts, and canvas banners an agent wrote.

Audit first, change only on evidence — existing authored material: decision records, reference guides, and any note carrying many relationships or measurements. Read it, list the specific problems, and revise only the notes with real defects. Most audits should change some notes and leave others untouched.

Never rewrite silently — raw capture where the words are the evidence: raw transcripts, voice memos, customer quotes, source evidence, legal text, and human-authored material where the voice itself is the record. Store these unchanged. If a reader needs a clean version, humanize the derived artifact, never the source.

Override language wins over all of the above. If the user says any of these, follow the instruction and stop:

  • "Save this as raw capture. Do not humanize it." → store verbatim.
  • "Audit X for AI writing. Do not change anything." → report only, no writes.
  • "Humanize X. Preserve timings, links and questions." → rewrite with the named things held fixed.
  • "Rewrite this in my personal voice." → apply a voice card on top of the floor.
  • "Use the neutral / company voice." → no personal voice, shared-artifact tone.

The four modes

ModeWhat you do
Draft cleanlyAuthor new prose already free of slop — the audit is baked into the first draft, not bolted on.
Audit onlyReport defects with locations and no edits. Hand the nuanced findings to the user.
Rewrite with preservationFix the flagged prose while the preservation gate holds every fact, number, ID, and link fixed.
Match a voice cardApply an optional personal or company voice on top of the clean floor.

Default to Draft cleanly for new prose and Audit only for existing material. Only rewrite existing notes once an audit has named the specific defect.


The workflow

New agent-generated prose (draft → audit → preserve → save)

  1. Draft for meaning and the note-type rubric (cnotes types show <Type>). Own the outline yourself before filling it in — see Structural slop.
  2. Run the anti-slop audit against the kill-list and moves below.
  3. Remove high-confidence pattern clusters, not isolated words. A single flagged word in otherwise clean prose is usually a false positive. A cluster (hype adjective + filler verb + rule-of-three in the same paragraph) is real slop.
  4. Run the preservation check — verify nothing in the preservation gate changed.
  5. Save with a # h1 title and readable relationship-mention titles.

Existing notes and canvases (audit → select → version → verify)

  1. Read the canvas digest (cnotes canvas digest <id>) and the note bodies.
  2. Audit only. Identify notes with clear problems; leave clean notes alone.
  3. Create a new version (cnotes versions create) only for the selected notes, with a change description saying what you fixed.
  4. Fix affected canvas text cards in place with cnotes canvas update-text (text cards have no version history).
  5. Verify numbers, names, IDs, links, mention titles, tables, uncertainty, and canvas layout survived unchanged.

The anti-slop audit

Slop is a bundle of habits, not a list of words. Scrub the vocabulary and a symmetrical skeleton still reads machine-built. Fix both.

The one rule under all of these

When you catch a hit, do not swap it for a quieter synonym — a calmer slop word is still slop. Replace it with one of three things:

  • a concrete noun (the graph, the version, the transcript),
  • a number with its baseline ("nine minutes to ninety seconds on the two-thousand-file repo"),
  • an active verb that names the behavior ("re-indexes on every edit", "links each claim to its evidence").

If you cannot make the line concrete, the line has no claim. Cut it.

Words — cut on sight

A. Hype / greatness adjectives (grade us instead of describing behavior): powerful, seamless, robust, scalable, cutting-edge, revolutionary, game-changer, innovative, world-class, best-in-class, effortless, magical, intuitive (self-claimed). Fix: state the behavior.

B. AI-slop verbs and nouns (filler the model reaches for): delve, leverage, harness, unlock, unleash, supercharge, streamline, elevate, empower, foster, tapestry, journey, landscape, ecosystem, realm, synergy, paradigm shift. Fix: name the action or the thing.

C. Slop connective tissue (announces a transition, carries no information — delete the phrase, keep the sentence): furthermore, moreover, additionally, in conclusion, it is important to note, it is worth noting, needless to say, aims to (→ present-tense verb), when it comes to (→ the subject itself), at the end of the day, in today's fast-paced world.

D. Category cliches (name the outcome and the pain, not the category): second brain, PKM, note-taking app, knowledge base, all-in-one workspace, digital brain, smart notes, AI notes, AI-powered notes, "X alternative".

E. Personal-ban tics (our own recurring habits): actually, simply, just (as filler), really (as intensifier), might, perhaps, kind of, thrilled, excited.

Moves — a sentence can pass every word check and still be slop

Banned moveDo instead
Em dashes in user-facing proserewrite the sentence; split it or use a period
Comparison framing ("unlike X", "the only X that")state what we do; name a shared category truth, no rival
Absolutes (everyone, nobody, always, the only)state the specific case and let it stand
Antithesis template ("not just X, it is Y")make the affirmative claim once
Negation-reversal with a clipped echo ("X was not the bottleneck. Y was.")state the claim by consequence, not as a "Not X. Y." flip
Rule-of-three triples ("Focused. Aligned. Measurable.")one claim that carries its own weight
Throat-clearing openers ("a thread on", "I want to share")open with the problem or the payoff
Thread-bro formatting ("1/", "follow for more")write as prose with a standalone first line
Engagement bait ("comment YES", "drop a fire emoji")end on the value, not the ask
Numbers with no baseline, date, or sourcepair every number with its baseline and where it came from
Both-sides hedge-seesaw presented as a viewcommit to a stance and name its cost
Exclamation points in feature copya period; the usefulness is the news

Structural slop — own your outline

Structure is the deeper tell than vocabulary. A tidy intro, three even sections, and a balanced conclusion pattern-match to generated text even after every banned word is gone. Write the spine yourself, then let the draft fill inside it. If the skeleton is symmetrical, break the symmetry before you touch the words.

Watch the over-correction

Humanizing fixes meaning and structure. Do not thesaurus-swap a banned word for a near-synonym (that trades one tell for another), and do not manufacture fake casualness in place of formal prose that was doing real work. Rewrite the sentence around the load-bearing fact, or leave it.


The three verifiable flags

These are the only defects a model can flag without knowing whether the claim is true. A precise-sounding but unsourced number passes every linter, so do not pretend to grade truth or calibration. Flag these three, and pair every concreteness check with a source check:

  1. Unredeemed abstraction — a comparative or magnitude word (better, faster, bigger, significant, most, many, soon, a lot) with no number, unit, baseline, and date. Demand the operand: better on which metric, from what baseline to what target, by when.
  2. Missing falsifier — on an empirical or decision claim, no statable "this is wrong if X by date D."
  3. Naked claim — a load-bearing claim with no linked Fact, Test, or Metric (no supporting relationship mention).

Surface these as questions for the author, not as silent rewrites — forcing a number where none exists only mints fake precision.


The preservation gate

Before saving any rewrite, confirm none of these changed. Each row is a real way humanization corrupts a note:

  • Numbers, dates, names — identical to the source.
  • Negations, uncertainty, scope — "not", "only", "up to", "we think", "in some cases" carry meaning; do not drop or invert them.
  • Legal / technical qualifiers — a hedge that is a legal or technical constraint is not slop. Keep it.
  • Checklists and structured writing — deliberate structure is not slop. Do not flatten a checklist into a paragraph.
  • Relationship mentions — titles and links intact. [NOTE-12: Title](relationship:type) must keep both the display ID and a readable title; never strip a mention to bare text or drop its link. And the TYPE must carry the verb: if the prose narrates "blocks" / "depends on" around a references chip or untyped [[NOTE-12]], retype the chip (relationship:blocks, [[blocks::NOTE-12]]) instead of leaving the verb in prose the graph can't see.
  • Tables and layout — tables, columns, and canvas placement survive.
  • Voice as evidence — a customer quote or raw memo keeps its original wording.

When in doubt, keep it and flag it in Audit only mode rather than rewriting.


Two layers: quality floor and personal voice

Removing slop creates a shared quality floor. It does not create a person's voice. Keep the two separate.

The shared quality floor (always)

The anti-slop audit above. Applies to everyone and every genre. It removes corporate fog, fake contrasts, forced triads, and chatbot residue.

Optional personal voice cards (only when asked)

A voice card is a local, genre-specific profile built from writing the author chose. One universal "sound like me" profile is too blunt: a Slack message, a strategy note, a public article, and a technical spec should not sound identical. A card can capture directness, sentence rhythm, formality, humour, preferred vocabulary, spelling, punctuation, heading style, and how strongly the author states opinions. Store it locally by default; share only when the author chooses. Never impose one person's voice on every user.

Shared artifacts use a neutral or company voice

Company-facing documents use a neutral CreatorNotes voice or a company voice, not an individual's profile. When the author is the team, write in a steady first person: attach each opinion to the specific decision or failure that produced it, so credibility comes from the work, not from adjectives. A bolt-on "I personally believe" in front of a generic claim still reads synthetic — the first person has to carry a real detail (a number, a named workflow, a decision and what it cost), or cut both the "I" and the claim.


CreatorNotes mechanics (deterministic — always apply)

These are objective and never need judgment. Enforce them on every write:

  • Canvas titles use the middle dot · (U+00B7) as a separator, never an em dash or en dash.
  • Every note starts with a # h1 heading — it becomes the title.
  • Relationship mentions include a readable title: [NOTE-123: Title](relationship:type), never bare NOTE-123.
  • Relationship type matches the narrated verb — prose "Blocks X" around a references/untyped chip is a graph bug, not a style choice: retype to relationship:blocks (or [[blocks::NOTE-123]]) and drop the redundant verb.
  • Canvas richtext and list descriptions name notes with relationship mentions — [NOTE-12: Title](relationship:verb) renders as a chip with the real title; a bare display id in card prose ("see RISK-64") is a defect.
  • Richtext stays card-sized — at most ~3 sentences, one heading; distinct items are N typed notes, not a wall of text.
  • No placeholder or artifact tokens — "lorem ipsum", "TODO", "[insert X]", "as an AI language model", or leftover template markers never ship in saved prose.
  • Timeline headlines are the platform's. Bodies are the agent's. A changeDescription, milestone --message, or operations --prompt is a verb-first past-tense headline (specific object in the first 40 characters, at most 110 characters), a blank line, then a body of at most 280 characters. No Created, Updated, Edited, or Changed. No dates, weekdays, or times. No leftover {displayId}. No em dashes. The platform rewrites a failing agent headline and leaves the body. Human-typed editor text only gets {displayId} filled. One change per headline.
  • Valid mention syntax and live links — no broken links, no malformed mentions.

Restraint is the most important behavior

The measure of a good pass is not the number of edits. It is the willingness to leave clean notes alone. A selective pass that fixes the three genuinely-slopped notes and leaves nine good ones untouched is a success. A pass that rewrites every note because a detector word appeared somewhere is the failure this whole reference exists to prevent.

If a note already reads as a real person wrote it, do nothing.

Other commands

Focus plans, files and images, setup, theme, config, and sending feedback to the CreatorNotes team.

Query the workspace (cnotes query)

Workspace-wide questions over every note's metadata, answered from a local SQLite copy instead of paged notes list reads. The first call copies the workspace (about a minute for 5,000 notes); later calls read the head and what changed (a second or two at that size), then the query runs in milliseconds. Never a note body: read those with cnotes notes get.

cnotes query pricing --type Decision,Insight        # words in the title; AND across fields
cnotes query --canvas CANVAS-12 --group-by tag      # notes on one canvas, by tag
cnotes query --made-by api --group-by month --json  # notes written with a cn_ key (CLI, MCP) are api
cnotes query --group-by Risk:status                  # a judged dimension; `unjudged` is a group
cnotes query --sql "SELECT type, COUNT(*) AS n FROM notes GROUP BY type ORDER BY n DESC"
cnotes query --sql tables                            # the tables and their columns

--json returns { total, groups, rows, sync, ms }; rows carry displayId, title, type, updatedAt and the group they fell in. --sql runs ONE read-only SELECT (a write is refused). --rebuild drops the copy and copies again; --forget deletes this workspace's copy. Needs Node.js 22.13 or newer.

With the copy on, cnotes notes list answers type, tag, text, pin and no-canvas lists from it too, with no extra step: inside 30 s of a sync and with no write from this machine since, it reads only the file; otherwise it syncs first, so your own writes always show. Dimension filters (--open, --answered, --needs-me) ask the server. stderr says which answered: From the local copy, synced 12 s ago. or From the server: <why>.

The copy is off until the PERSON allows it: cnotes config set-local-db on (which copies the current workspace right away). If cnotes query refuses with that hint, tell the user what the copy holds (metadata, never a body) and ask them to run it; do not work around it with notes list. set-local-db off deletes every copy on the machine.

Focus plans (cnotes focus)

Protected flow-state blocks, private to the user. The Focus calendar overlays plans on recorded attention; kept/missed is derived, so there is no "complete" verb.

# Plan a block. Link the prepared canvas so opening the block orients in
# seconds — and --target to land it on the exact card the block works on.
cnotes focus plan --start 09:00 --duration 90m \
  --goal "Decide the pricing wedge" --canvas CANVAS-12

# --target names a NOTE-… or SECTION-… display id ON that canvas (an
# Attention card, a Decision). Needs --canvas; validated at plan time (a
# ref not on the canvas is refused). The calendar block then renders
# "CANVAS-80 → ATTENTION-6J4 · <title>" and clicking it opens the canvas
# landed on that card.
cnotes focus plan --start 09:00 --duration 45m \
  --goal "Approve the customer target" \
  --canvas CANVAS-80 --target ATTENTION-6J4

cnotes focus list [--days <n>] [--past <n>]   # target shows in the canvas column
cnotes focus cancel <planId>
# --start takes "HH:MM" (next occurrence), "tomorrow HH:MM",
# "YYYY-MM-DD HH:MM", or full ISO; --duration takes "45m", "90m", "1.5h".
# list --json rows: { id, startMs, endMs, goal, canvasViewId, canvasName,
#   canvasDisplayId, targetRef, targetTitle, createdBySource, createdAt }.
# targetTitle null while targetRef is set = the card drifted off the board
# since planning — re-target or re-plan rather than trusting the block.

Keep the plan row thin. Time, goal, destination — nothing else. The durable clues (decisions, criteria, "complete when") live IN the target note, never in the goal string: a display id pasted into the goal is inert prose the graph cannot see, while --target is the clickable, validated form of the same intent. A plan with no --canvas renders as a non-clickable block; that is by design, not a bug — there is nothing to open.

Files (cnotes files / cnotes f)

# Upload an image to the workspace (max 5MB, supports .jpg .jpeg .png .gif .webp)
# An SVG (a rendered diagram, say) is refused: convert it first, e.g. rsvg-convert -o diagram.png diagram.svg
cnotes files upload <path> [--markdown]   # --markdown outputs ![filename](url) syntax

# Read an image that's INSIDE a note. A note's content prints images as
# ![alt](/api/images/convex/<storageId>) — that path is your storage id. Use
# this to turn it into actual pixels you can look at (authenticates with your
# CLI key, same as `cnotes notes get` does for text).
cnotes files get <storageId> [-o <path>]   # download the bytes (default: ./<storageId>.<ext>)
cnotes files get <storageId> --url         # just print a directly-fetchable URL
cnotes files get '/api/images/convex/<storageId>'   # paste the path straight from a note

To "see" an image in a note: cnotes notes get NOTE-X to find the /api/images/convex/<storageId> reference, then cnotes files get <storageId> -o /tmp/img.png and open the saved file.

Init & MCP (cnotes init / cnotes mcp)

# Sign in, pick a workspace, install the agent skill.
# On a terminal this walks through the questions. With no terminal, or with
# --non-interactive, it does not open a browser: it prints a sign-in URL and
# exits 2 until you run the heredoc from `cnotes init --help`.
cnotes init
cnotes init --help
cnotes init --non-interactive

# Who is signed in. Exit 0 when a token is stored, exit 2 when signed out.
cnotes auth status
cnotes whoami

# Configure MCP server for AI tools (also handled by cnotes init on a terminal)
cnotes mcp setup          # Claude Desktop
cnotes mcp setup-codex    # OpenAI Codex

Theme (cnotes theme)

Personal UI theme preference (synced to your account, applies on next browser reload — does NOT live-update an open tab).

# List built-in presets — current one is marked with *
cnotes theme list

# Show the active theme + any per-token color overrides
cnotes theme get

# Switch to a built-in preset
cnotes theme set <id>          # e.g. dracula, ember, catppuccin-mocha, sage, light, dark

# Override a single color (switches the theme to "custom" automatically)
cnotes theme set-color <token> <hex>   # e.g. cnotes theme set-color primary "#ff00aa"

# Drop all custom overrides and return to the base preset
cnotes theme reset

Editable tokens (anything else is rejected with 422): background, card, popover, muted, border, sidebar-background, primary, primary-foreground, ring, accent-teal, foreground, muted-foreground, prose-body, prose-heading, code-background, code-foreground, blockquote-border, destructive, success, warning, info.

set-color merges into existing overrides — call it repeatedly to build up a custom palette. reset returns to the preset that was the base of your custom theme (or dark if you started from scratch).

Config

cnotes config get-server
cnotes config set-server <url>
cnotes config get-local-db            # on | off: may this machine keep a copy of workspace note metadata?
cnotes config set-local-db on|off     # off deletes every copy under ~/.cnotes/local-db

Feedback (cnotes feedback)

# Report friction, a bug, or praise from inside an agent session.
# Routes to the CreatorNotes team's central feedback inbox (server-side routing —
# you never need a canvas or workspace id). See "Closing the loop" below
# for WHEN to use this and the rule to ask the user first.
cnotes feedback submit --severity <blocking|high|medium|low|praise> --area <area> --title <text> \
  [--body <md> | --body-file <path> | --body-stdin] [--model <id>] [--rating <1-5>]

# Examples
cnotes feedback submit --severity blocking --area cli/canvas \
  --title "canvas list 500s" --body-file ./report.md --model <your-model-id>
echo "Loved the batch @key cross-links." | cnotes feedback submit \
  --severity praise --area cli/notes --title "Batch create is the right primitive" --body-stdin

Closing the loop: submit feedback when you hit friction

CreatorNotes is built for humans and agents, so feedback from an agent is real feedback. When you hit something worth reporting, offer to log it with cnotes feedback submit (above).

When to offer it (be selective, not noisy):

  • A reproducible bug: a command 500s, returns wrong/empty data, or a documented path doesn't behave as described.
  • A real capability gap that blocked the task (not a transient blip, not your own mistake — rule those out first).
  • Standout praise is welcome too, but lead with friction; it's the actionable signal.

Always ask the user first. The report is attributed to them and goes to the CreatorNotes team. Say in one line what you'd report and ask if they want it sent. Never file silently, and never file the same issue twice.

Make it directly fixable. In the --body (markdown), include:

  • What happened — the exact command you ran and the exact error/behavior.
  • Root cause (suspected) — your best read from the outside; say what's uncertain.
  • Suggested fix — what would resolve it.
  • Severity — who/what is blocked.

Set --severity honestly (blocking only if it stops the task), --area to where it bites (e.g. cli/canvas, editor, api/notes), and --model to your own model id so the team knows which agent reported it. One note per issue.

Notes, versions and types

Read this before creating, reading or revising notes: batch create, --expand reads, versions, types and rubrics, markdown rules.

Notes (cnotes notes / cnotes n)

# List notes (default 20, excludes drafts)
cnotes notes list [--search <query>] [--type <type>] [--tags <csv>] [--pinned] [--limit <n>]
cnotes notes list --no-canvas [--type <type>] [--tags <csv>] [--limit <n>]   # notes on no canvas at all

# Get one or more notes by display ID (always returns an array, in input order,
# in a single round-trip). Pass one ID or many — never call this in a loop.
cnotes notes get <id> [<id>...] [--all-versions] [--content-only]
cnotes notes get MEETING-12
cnotes notes get MEETING-12 PRD-3 IDEA-7

# Load a note together with the notes it rests on. --expand follows outgoing
# relationship rows and inline [[rel::ID]] / [[ID]] links (a bare [[ID]] is
# references). Each linked note is labelled `assumes -> OBSERVATION-6K13`.
# --depth N (default 1, max 4) walks further, deduped, and stops at 40 notes
# with a notice when truncated. --rel assumes,depends-on limits which slugs
# are followed. --summary keeps the title and the first real prose paragraph
# of linked notes (the requested notes stay whole). It skips leading headings,
# blank lines, and chip-only lines, so a Metric that starts
# `## Primary measure` returns the sentence after it, not the heading.
# Heading-only notes fall back to the last heading text; list-only notes
# fall back to the first list. --incoming also includes notes that
# link to the target (`assumes <- OTHER-1`).
cnotes notes get DECISION-6KN --expand
cnotes notes get DECISION-6KN --expand --depth 2 --rel assumes,depends-on
cnotes notes get DECISION-6KN --expand --summary --incoming --json
cnotes notes get DECISION-6KN --expand --content-only
# --json with --expand is an object, not the bare array:
# { notes, linked, truncated, cap, omitted, notice }.
# Each linked entry is { displayId, relation, direction, depth, from, label, links, note }
# and note matches a notes get row. A truncation sets truncated and notice, and
# the notice is also printed on stderr so stdout stays valid JSON.
# Reads also cap at 40 relationship rows/inline mentions per note and 512
# candidates across the walk. These limits set truncated and notice too;
# omitted counts only discovered notes skipped at the 40-note output cap.
# Without --expand, reads stay a bare array, so a single note is always at .[0].

# Reading a LARGE note/transcript: --content-only prints just the raw markdown
# (no JSON envelope, no metadata chrome), so the terminal won't clip a giant
# JSON line. Best for transcripts you want to read or chunk. With --expand,
# linked notes follow the root, each introduced by an HTML comment of its label.
cnotes notes get TRANSCRIPT-1 --content-only
cnotes notes get TRANSCRIPT-1 --content-only > /tmp/transcript.md   # then chunk the file

# A REPLACED note says so. When a live supersedes / superseded-by / invalidates /
# replaces link marks a note as replaced, `notes get --json` carries
# `supersededBy: [{ displayId, title, type, relationship, relationshipId }]`
# (newest replacer first), the plain output prints "Superseded by: …", and
# `canvas read` prints "> Superseded by …" under that note's header. Read the
# replacer before relying on the old note. The field is never inside the body,
# so --content-only and edits are unaffected.
# NO field does NOT mean current: it means no link says otherwise. A note
# superseded only by a prose banner has no field, so still read banners. When
# you write a note that replaces another, add the link
# (`[OLD-1: Title](relationship:supersedes)`) so the next reader is told.

# Which canvases is this note on? The agent mirror of the editor's "Canvases"
# affordance. Container-aware: counts the note whether it's placed directly OR
# nested inside a list/grid container. Each row has { id, name, via, viewType,
# visibility, isHome, goal, updatedAt }; `via` = node | list | both. Feed `id`
# into `canvas get/read`. (See "See how a found note connects" under Relationships.)
cnotes notes canvases <displayId>
cnotes notes canvases INSIGHT-1 --json --fields id,name,via

# Who relied on this note, and what they built from it — the read-side
# counterpart to the timeline's "which run wrote this version". Ask it AFTER
# editing a note that other work was written from: it returns the agent runs
# that read an older version, plus the notes each run went on to write. That is
# the "who do I have to tell" list, and it catches reliance the relationship
# graph cannot, because a note an agent read and never cited leaves no mention.
# --json: { note, readers[], staleRuns[], unattributedStaleReads }. A reader is
# `stale` when the note advanced past the version it was served — drift, not
# obsolescence. Reads made before the agent opened its run carry no run id and
# are counted rather than listed. Agent reads only (CLI + MCP); a person reading
# in the browser is deliberately never recorded here.
cnotes notes reads <displayId>
cnotes notes reads FEATURE-1 --quiet    # just the display IDs of the derived notes

# Create one or more notes (always batch-shaped).
# FIRST: read each target type's rubric, then author to it. Do not skip this —
# the create response echoes rubrics after save, which is too late to guide
# the draft. operations begin --json also returns typeRubrics for the workspace.
cnotes types show Insight
cnotes types show PainPoint
# Then create. Pass --notes as a single JSON object or an array. Title comes
# from the # h1 heading in each note's markdown. Use
# [@key: Title](relationship:<type>) to cross-reference another item in the
# same batch — @key is replaced with the new note's display ID after creation.
# Pick <type> from the verb your own prose uses ("blocks", "depends-on",
# "invalidates" — `cnotes rel types` lists the vocabulary; free-form, unknown
# types auto-create). `references` = pure citation ONLY: a verb narrated in
# prose around a references chip is invisible to backlinks and `rel list`.
cnotes notes create --notes '{"key":"A","type":"Insight","markdown":"# Solo note","tags":["ux"]}'
cnotes notes create --notes '[
  {"key":"A","type":"PainPoint","markdownFile":"./problem.md"},
  {"key":"B","type":"Insight","markdown":"# Fix\n[@A: Problem](relationship:resolves)"}
]'
# Each item: {key, type, markdown | markdownFile, tags?, changeDescription?}.
# changeDescription is version 1's timeline line: a verb-first headline
# (specific object in the first 40 characters, at most 110 characters), a
# blank line, then a body the agent owns. `{displayId}` is filled in. A
# failing agent headline may be rewritten; the body is kept. The response
# includes `headline` and `headlineWarnings`. Omit the line and the platform
# later writes one. Human-typed editor text only gets the token filled.
# On validation failure the response lists every bad item by index + key + field
# (and `details.items` in --json mode), so you can fix the whole batch in one shot.
# Same batch again in the same run returns the first notes (alreadyExists: true), not a copy; after a reset or 5xx (`mayHaveCommitted: true`) re-run with the printed `--idempotency-key`.
# Each .results[] item carries .type + .rubric — keep them when filtering JSON.
# Where the deployment turns the rubric check on, each item also carries
# .quality: {level 1-4, label, missing[], nextStep, unsure}. `missing` quotes the
# rubric clauses the note does not yet do, weakest first. Fix those with
# `versions create` before placing the note (the new version is graded again
# and comes back with its own .quality). `unsure: true` means the level is a
# guess: read the note yourself instead of rewriting on its word. Without
# .quality, grade yourself as below.

# THEN: grade what you just wrote and revise before ending the run.
# You are the grader. Grading spends no credits. `cnotes quality` does not
# call a server model. It returns a gradeRequest on each ungraded note:
# task "grade_against_rubric", the type rubric, the note content, and the
# instruction "Grade this against the rubric". The create response's
# .selfCheck is this command. Read .items[].gradeRequest (it also sits on
# .gradeRequests). Do not drop it when you keep only the display id.
cnotes quality INSIGHT-12 PAINPOINT-4 --fail-below 4 --json
# Exit 3 until you record a grade. Then:
cnotes quality record INSIGHT-12 --graded-version 4 --level 3 --criteria '[{"criterion":"Evidence","reason":"Grounded in two tickets."}]' --feedback "The pattern is named." --next-step "Say what would falsify it."
cnotes quality --canvas CANVAS-60 --json
cnotes quality --all --limit 200 --no-grade
# Structural codes (free, no credits): missing_title, empty_body, bare_id,
# unknown_type, no_rubric, superseded, missing_section.
# missing_section fires only for headings named in a rubric line
# "Required sections: Problem; Options". A rubric without that line
# requires no headings.

# Update metadata only (not content — use versions for that)
# NOTE: --title is NOT available. Title is read-only, derived from the # h1 heading in version content.
# To change a note's title, create a new version with a different # h1 heading.
cnotes notes update <id> [--type <t>] [--tags <csv>] [--archive] [--unarchive] [--pin] [--unpin]

# Delete (archive)
cnotes notes delete <id>

# Bulk archive/unarchive multiple notes at once
cnotes notes bulk-archive --ids '["MEETING-1","PRD-3","IDEA-7"]' [--unarchive]

# Bulk retype multiple notes to a new type (atomic per note: creates new, archives old, cross-links)
cnotes notes bulk-retype --ids '["NOTE-1","NOTE-3","NOTE-5"]' --type Insight

# Text search
cnotes notes search <query> [--type <type>] [--limit <n>]

Versions (cnotes versions / cnotes v)

# List versions
cnotes versions list <noteId>

# Create a new version (updates note content)
cnotes versions create <noteId> --description "Added the pricing objection from the call" --markdown-file <path.md>
cnotes versions create <noteId> --description "what changed" --markdown "<short content>"  # inline (short only)
cnotes versions create <noteId> --description "what changed" --markdown-stdin < changes.md

Types (cnotes types / cnotes t)

# List all types (supertags) in the workspace
cnotes types list

# Show one type's quality rubric (validationPrompt) — read before authoring that type
cnotes types show <name>

# Create a new type (prefix is auto-derived from display name)
cnotes types create <name> [--display-name <name>] [--description <text>] [--color <hex>]

# Update an existing type (prefix updates automatically when display name changes)
cnotes types update <name> [--display-name <name>] [--description <text>] [--color <hex>]

# Retype a note (atomic: creates new note with target type, archives old one, cross-links both)
cnotes notes retype <displayId> --type <NewType>

Dimensions

A type can carry dimensions: readings every note of the type gets, shown by notes get under the title and filterable with notes list --dimension.

  • Link dimensions are built in and read from live links: a Question is answered when a note links it with answers (or the Question links its answer with answered-by); any note is supersededBy when a newer note says supersedes. Write those links and the reading follows.
  • Judged dimensions are questions you add to a type, beside its rubric. A decision model answers each one about every note of the type on create and on each new version. At most three per type; admin or owner only, like the rubric.
# Add one: boolean (yes / no / unsure), category (one of --options), percentage (0% to 100%)
cnotes types dimension add Decision --key revenue_risk --kind boolean \
  --prompt "This decision puts current revenue at risk if it turns out wrong."
cnotes types dimension add Opportunity --key stage --kind category \
  --options discovery,shaping,delivery --prompt "Which stage is this opportunity at?"

# See a type's dimensions (also in `types show`)
cnotes types show Decision

# Filter by any dimension
cnotes notes list --type Decision --dimension revenue_risk=yes
cnotes notes list --type Slice --dimension readiness=">=50"
cnotes notes list --needs-me          # open Questions whose "Whose call" names you

# Judge the existing notes too: first see the count and cost, then ask the person
cnotes types dimension backfill Decision revenue_risk          # estimate only
cnotes types dimension backfill Decision revenue_risk --yes    # one model call per note

cnotes types dimension remove Decision revenue_risk

Write the prompt the way you would a rubric clause: one plain statement about one note, checkable from the note's own text ("names the customer who asked", not "is good"). Adding a dimension judges only new and edited notes. Before you judge existing notes, tell the person how many and what it costs (the add output says both) and let them choose all existing or only going forward. Changing the prompt, kind or options makes earlier answers stop counting until each note is judged again; a new label alone keeps them.

Note IDs

  • Notes have display IDs like MEETING-12, PRD-5, IDEA-3 — use these in commands.
  • Convex IDs (e.g., n17a43p7aenrxyr...) also work but are less readable.
  • When creating notes, capture the display ID from the output for subsequent commands.

Note Types

  • The --type parameter must reference an existing supertag in the workspace. Free-text type strings are rejected.
  • Pick the type, then read its rubric, then write to it — in that order. Each type carries a validationPrompt (its quality rubric: what a good instance contains). Once you know the type, run cnotes types show <Type> (or read typeRubrics from cnotes operations begin --json / validationPrompt from cnotes types list --json) and author to that rubric so the note lands well-formed. Example: a Decision needs the alternatives weighed, the rationale, and a revisit trigger, and it cites the cards it depends on (see Decisions). cnotes notes create --json also puts the rubric on each .results[] item (type + rubric) and as a top-level rubrics array. Do not cut the output down to only displayId until you have checked .results[].rubric. After the write, you grade: cnotes quality hands back gradeRequest (rubric + content + "Grade this against the rubric") and spends no credits. Record the grade with cnotes quality record. A Decision rubric that declares Required sections: fails the free structural check until those ## headings are present.
  • If the type you need doesn't exist, create it first: cnotes types create "MyType" --display-name "My Type"
  • Type names are case-sensitive and use PascalCase (e.g., Update, PainPoint, Meeting).
  • The display ID prefix is auto-derived from the display name: spaces become hyphens, all uppercase (e.g., display name "Pain Point" → prefix PAIN-POINT, display name "Idea" → prefix IDEA).
  • Prefix is immutable once notes of that type exist. To change a note's type, use cnotes notes retype <displayId> --type <NewType>.

Content via Markdown

  • Markdown is the only content format on the wire. For multi-line content, write to a file and pass the path via markdownFile:
    cnotes notes create --notes '[{"key":"A","type":"Note","markdownFile":"./note.md"}]'
    
  • Use inline markdown only for short, single-line content.
  • Always start markdown content with a # h1 heading. This becomes the note title. The title is a headline, not a sentence: at most 80 characters, naming the claim or the object, with no consequence clause (", so…", ", because…", ", which…"), no display id and no em dash. The body's first sentence carries the why. The server refuses an agent's note or version whose title breaks this (field: "title" in a batch) and the message says what to change; a person typing in the editor is never refused. Follow proper markdown structure: ## h2 for major sections, ### h3 for subsections. Well-structured heading hierarchy is expected for all notes.
  • Relationship mentions in markdown: Use [NOTE-123: Title](relationship:<type>) — plain text "NOTE-123" does NOT create clickable mentions, and the title-less form renders as "Untitled". Match <type> to the verb your prose uses (blocks, depends-on, …); references is for pure citations. The typed wiki form [[blocks::NOTE-123]] is equivalent — it's how note bodies serialize mentions back to you, and an untyped [[NOTE-123]] is a plain reference.
  • The classic miss: capturing an answer/outcome that lives in another note. "The answer is the packaging decision in DECISION-24" saved as plain prose creates no chip, no relationship, no backlink — the graph never learns the question was answered. Write the mention with the verb as its type: Answered by [DECISION-24: Title](relationship:answered-by). notes create and versions create detect bare IDs that name real notes and return them in plainTextIdWarnings (stderr + --json), each with a paste-ready rewrite carrying the note's actual title — treat every entry as a fix instruction (save a corrected version) unless the bare ID is deliberate prose.
  • Quoting another note's exact words: write a quote block, the words in a blockquote and a last line citing the version they came from with a quotes mention pinned to it. Read the version number and its time from cnotes versions list <id>; the words must be that version's, verbatim:
    > We lose them at the invite step
    >
    > — from [[quotes::TRANSCRIPT-4@v5]] v5, 12 Oct 2026, 4:55 pm
    
    It is a hard copy: the version never changes, so the block never goes stale, and saving it records a quotes relationship pinned to v5. Say what the quote means in your own sentences around it.
  • Reuse an existing relationship type before inventing one. cnotes schema lists the workspace's slugs most-linked first (cnotes rel types lists every one); pick the closest existing label. The type slot is free-form, so an unknown label is auto-created as a custom workspace type rather than dropped — but that means a typo or a needless synonym (support vs supports, relates-to vs related-to) silently pollutes the workspace's type vocabulary forever. Only coin a new type when nothing existing fits.
  • Names are normalized to lowercase + hyphens, so Depends On, depends on, and depends-on all resolve to the same type. When cnotes notes create does create new types, it reports them (+ created N new relationship types: …) — treat that line as a prompt to double-check you didn't mean an existing one.
  • Common built-in types to reach for first:
    • depends-on / blocks — dependency ordering between notes
    • references — pure citation with no claimed relationship (the type [NOTE-123] gets when you name none)
    • implements — a note that delivers on what another identifies (e.g., feature → requirement, solution → gap)
    • derived-from — a note created based on another's content
    • extends — adds depth or detail to another note's topic
    • invalidates / supersedes — supersedes or contradicts another note
    • related-to — loose thematic connection
    • duplicates — notes covering the same thing
  • Simple mention shorthand: [NOTE-123] or [NOTE-123: Title] (defaults to "references")

Updating Content vs Metadata

  • To change note content (including title), create a new version: cnotes versions create <id> --description "..." --markdown-file ./updated.md
  • Title is derived from the # h1 heading in the latest version — to rename a note, create a new version with a different heading.
  • To change note type/tags/pin/archive, use: cnotes notes update <id> --type "..." --tags "..."
  • These are separate operations by design.

Operations, runs and the timeline

Read this for the detail behind operations begin/end: when to wrap, two agents on one machine, canvas agent-runs, verifying attribution, and reading the timeline.

When an AI agent is doing a batch of work — creating notes, modifying 2+ entities, or building a canvas with content — wrap it in a workspace-level operation. The batch then shows up in the timeline as a single legible row with intent + evidence + count, and the user can later revert the whole run as one unit. Unwrapped, each write is an isolated timeline row with no shared intent or batch revert — and on canvas and other multi-write paths nothing forces the wrap, so you must open the run yourself.

This is enforced for every write. A note create, version, metadata change, archive, retype, move, link removal, dependency review or canvas change from a key is rejected with operation_required (HTTP 422, exit 3) unless a run is active or the command carries --intent. Comments, review requests, uploads, types and focus plans are not. Begin one first; the active run then auto-attaches to every subsequent write.

When to wrap

ActionWrap?
Reading: digest, get, list, activity, timeline, search, rel listNo — read-only
Any cnotes notes create (even a single note)Yes — enforced. Creates run through the batch endpoint, which now rejects writes with no active run
A single notes update / tag / pin / archive changeYes — enforced. Pass --intent "<headline>" on it, or wrap it with your other writes
Two or more writes that belong to one intent (multi-note batch, canvas build-out, multi-step refactor)Yes
Canvas mutations done as part of an agent taskYes — even one canvas command, if it's part of a larger batch

One write is its own run with --intent. When the whole task is a single write command (notes create, update, delete, retype or move, versions create, rel rm, deps review, canvas create, update, delete, archive, place, arrange, an add-*, update-*, move-* or remove-* canvas command), pass --intent "<headline>" on it instead of bracketing it: the CLI opens a run with that headline, runs the command under it, and closes the run even when the command fails. Removals (bulk-remove, remove-node, remove-edge) get a canvas run with a before snapshot, the same as canvas agent-run wrap, so they can be undone. When a run is already open, the write joins it and --intent is not used as a headline. cnotes <command> --help lists the flag on every command that takes it.

The pattern

cnotes operations begin opens a run; every subsequent cnotes write on this server attaches to it automatically, across any shell, until you call cnotes operations end. No env var to export, no eval trick — the active run is persisted in ~/.cnotes/config.json per server, so stateless callers (separate Bash invocations, disjoint tool calls) all attach correctly.

# 1. Open the run with the user's intent as the prompt.
#    --json includes typeRubrics: every workspace type that has a validationPrompt.
#    Read the rubrics for the types you will author BEFORE drafting.
#    With --prompt it also returns a brief (brief.notes, brief.canvases,
#    brief.changes): the notes, canvases and recent changes most relevant to the
#    task. Read it BEFORE suggesting anything; a Decision, Guide or Metric there
#    may already settle what you were about to propose. Not opening a run?
#    `cnotes brief "<task>"` returns the same brief, read-only.
cnotes operations begin --prompt "Reorganized the roadmap into quarters" --json
cnotes types show Insight   # or pull from .typeRubrics in the begin response

# 2. Do the work. Every cnotes write here attaches to the run automatically.
cnotes notes create --notes '[ ... ]'
cnotes canvas add-text <canvasId> --content '# Q1'
cnotes canvas place <canvasId> --spec /tmp/q1.json
cnotes canvas add-edge <canvasId> --source <a> --target <b> --label "depends on"

# 3. Self-check the notes you wrote. You grade them. This spends no credits.
#    `cnotes quality --json` returns gradeRequest on each ungraded item:
#    the rubric, the note content, and "Grade this against the rubric".
#    Place the note on the ladder (1 Fragment, 2 Taking shape, 3 One move
#    from strong, 4 Strong). Write a reason for every criterion. Record it.
#    gradedBy becomes the open run id (this operation), or your agent name
#    when no run is open. Then run quality again. Exit 3 means revise with
#    versions create, record a new grade, and check once more.
cnotes quality INSIGHT-12 --fail-below 4 --json
cnotes quality record INSIGHT-12 --graded-version 4 --level 3 --criteria '[{"criterion":"Evidence","reason":"Grounded in two tickets."}]' --feedback "The pattern is named." --next-step "Say what would falsify it."

# 4. Close the run when done.
cnotes operations end

Resume the chat that produced the run. operations begin auto-stamps agentSession from the host agent when it can: Claude Code via CLAUDE_CODE_SESSION_ID / the SessionStart hook (cnotes claude-hook), Cursor Agent via the openable cloud agent id (bc-…). Inside a subagent, CURSOR_CONVERSATION_ID is sand-subagent-… or a child id, and https://cursor.com/agents/<that> is "Cloud Agent not found", so begin stamps the parent id from the agent metadata socket instead. The timeline then offers resume: claude --resume <id> (copy) or https://cursor.com/agents/<bcId> (open). Override with CNOTES_AGENT_SESSION_ID + CNOTES_AGENT_SESSION_KIND when needed.

Always close the run. If begin succeeded, end must run — otherwise the operation stays running and subsequent cnotes operations begin calls will refuse (with a clear "active run already exists" message naming the ways out). The server-side reaper marks a stale running row as abandoned after about an hour; begin checks the server and forgets such a run on its own, so a crashed earlier session never needs --force. cnotes operations open lists the runs this machine has open with the server's view of each; --prune forgets the ones no longer running.

If something goes wrong mid-batch and you can't recover: cnotes operations begin --force ends the prior run server-side and starts a fresh one. Only do this to a run you abandoned yourself: --force ends whichever run holds the active slot, which may be another agent's.

Two agents on one machine. Each run is tracked on its own. The active run (plain begin) is only the implicit default for writes that carry no run id. A second agent opens its own run with --detached and carries the returned id in CNOTES_AGENT_RUN_ID (or passes it to end --agent-run-id); a detached run never touches the shared active-run pointer, so neither agent can clobber or close the other's run:

RUN=$(cnotes operations begin --prompt "Triaged the inbox" --detached -q)
CNOTES_AGENT_RUN_ID=$RUN cnotes notes create --notes '[ ... ]'
cnotes operations end --agent-run-id $RUN

end resolves the run from --agent-run-id, then CNOTES_AGENT_RUN_ID, then the active pointer, and refuses (exit code 5) a run this machine did not open — only the actor who opened a run can end it server-side too. Ending one run never clears or ends another. --force on end is the explicit override for a run opened elsewhere.

The draft you work in is shared the same way: draft open and draft checkout save it for every terminal on the login. A second agent that should work live sets CNOTES_PROPOSAL=none; one that works in its own draft sets CNOTES_PROPOSAL=<draft id> (drafts.md).

Agents you start. When you start other agents (subagents, a swarm, a scheduled bot), give each one its own key instead of yours: cnotes auth keys create --name "<who it is>" mints a key pinned to the active workspace (or -w) that lives 2 hours unless --expires says otherwise (up to 24h). Every run that agent opens shows its name in History, so the person can tell five agents of one brand apart. -q prints only the key; the agent uses it through the environment and opens its own detached run:

KEY=$(cnotes auth keys create --name "Seat 3 · Notion PM" --expires 90m -q)
CNOTES_TOKEN=$KEY CNOTES_WORKSPACE=<workspaceId> cnotes operations begin --prompt "Reviewed the pricing story" --detached -q

A child key cannot mint keys, list or revoke them, or reach another workspace. cnotes auth keys revoke <yourKeyId> revokes every child it minted, and cnotes auth keys list marks which keys are children.

To trace a change back to the agent that made it, read the BY column (in JSON, author.agentName) of cnotes timeline, cnotes versions list <note> and cnotes canvas activity <canvas>: a change made under a child key reads "Deniss via Seat 3 · Notion PM". Under a child key, cnotes whoami prints the agent's name and the account it acts for.

Writing a good --prompt

The prompt is the timeline row's headline, and the server lints it like every headline: start with a past-tense verb and name the specific object, in the user's terms for that object. An imperative ("Re-cite the notes ...") is accepted but warns not_verb_first.

  • "Built a fan-out under the Goals heading with Q1 milestones"
  • "Reorganized the roadmap canvas into a SWOT layout"
  • "Added a risks list linked to the parent goal"

Avoid generic prompts like "make changes" or "update canvas" — they make the timeline useless.

Verifying it worked

After your run, the user (or you) can:

  • Look at the timeline panel in the workspace → the run appears as one row with a bot icon, your prompt as the headline, and a "N notes" count
  • Expand the row → each member note appears nested
  • Run cnotes operations get <agentRunId> → confirms affectedNoteCount reflects everything the run touched

If the timeline shows individual note rows instead of one grouped run, the writes didn't attach — usually because begin was never called, or end was called too early (before the writes).

Read before you write

cnotes operations read-first [--since 7d] reports how many agent runs that wrote notes read something first, and lists the "cold" runs that wrote without reading. A run counts as read-first when a read carries its run id, or when the same person's agent read in the workspace from 30 minutes before the run until it ended. It also reports orientation: the median reads before a run's first write, and how many runs that changed an existing canvas read it first. Reading the canvas you are about to change, before you change it, is what keeps your run off the cold list and in the second count.

Canvas agent-runs

# Agent-run bracketing — group a sequence of canvas changes into one operation
# with before/after snapshots so the user can see "AI did X" and revert as a unit.
# REQUIRED when an AI agent modifies a canvas — but an open `cnotes operations
# begin` run ALREADY satisfies it (same active-run pointer; canvas writes
# attach to it). Only bracket here when no operations run is open; a second
# `begin` on top of one refuses with CONFLICT. Notes directly below the block.
cnotes canvas agent-run wrap  --canvas <id> --prompt "<intent>" -- <command>   # one-shot
cnotes canvas agent-run begin --canvas <id> --prompt "<intent>" [--rationale "<plan>"]  # multi-step
cnotes canvas agent-run end   --canvas <id>   # no --run-id needed; ends the active run
cnotes canvas activity <canvasId>   # inspect operations + attribution after the fact

begin persists the run as the active run for this server (the same pointer cnotes operations begin uses), so every subsequent cnotes write attaches to it automatically — across separate shells and stateless tool calls, with no env var to thread through. end closes that active run, so it needs no --run-id. Only one run can be open at a time: a second begin refuses with exit 5 (CONFLICT) rather than silently clobbering the first — pass --force to end the prior run server-side and start fresh.

Prefer wrap for a single command (it auto-closes even on failure, and its run is scoped to the subprocess, so it nests correctly inside an open operations run). Use begin/end when the batch spans several commands.

Always close the run. If you can't, a later begin --force ends the stale one and starts clean. Verify attribution with cnotes canvas activity <id>: writes made inside a run show operationType: agent.run / actorSource: agent. If you see manual.edit / actorSource: human, the bracket did not take and the timeline is crediting your edits to the user.

Timeline (cnotes timeline / cnotes tl)

# Show recent changes (default: last 7 days)
cnotes timeline [--since <when>] [--until <when>] [--type <noteType>] [--note <filter>] [--limit <n>]

# Examples
cnotes timeline                        # last 7 days
cnotes timeline --since 30d            # last 30 days
cnotes timeline --since 2w --type Meeting   # meetings in last 2 weeks
cnotes timeline --since 7d --note standup   # filter by note name
# Supported --since formats: relative (7d, 2w, 3h, 30m), ISO date (2026-03-21), unix ms

# Search the change log by MEANING (not just a date range). Answers "when did we
# change X, and why" — a hybrid keyword + semantic search over the change
# descriptions. All-time by default; add --since to bound the window.
cnotes timeline --query "pricing framing"          # find the change, get NOTE + version
cnotes timeline --query "auth decision" --since 30d  # search within a window
# --json returns { mode: "search", query, results: [{ displayId, versionNumber,
#   changeDescription, snippet, createdAt, score, ... }] }

Relationships and dependencies

Read this when linking notes, reading backlinks, judging whether a link still holds, or checking dependents after an edit.

Relationships (cnotes relationships / cnotes rel)

# Backlinks + outbound links for a note. This IS the editor's "Backlinks"
# affordance for agents. --json returns { incoming, outgoing, all }:
#   incoming → notes that link TO this note (backlinks — "what cites me")
#   outgoing → notes this note links to ("what I cite")
# Each row carries the related note inline so you rarely need a follow-up fetch:
#   { id, direction, origin, live, isStale, madeAgainstVersion, relatedNoteCurrentVersion,
#     relatedNote:      { displayId, title, type, workspaceName },
#     relationshipType: { name, label } }
cnotes rel list --note <displayId>
cnotes rel list --note INSIGHT-1 --json --fields incoming   # just the backlinks

# By default you see links that hold RIGHT NOW. --history adds the superseded ones.
cnotes rel list --note <displayId> --history
cnotes rel list [--type implements] [--limit <n>]        # workspace-wide

# Delete a relationship by id (ids come from `rel list`).
cnotes rel rm <relationshipId>

# List the relationship types defined in the workspace. Check this BEFORE
# using a relationship:<type> label so you reuse an existing type instead of
# creating a near-duplicate.
cnotes rel types

Read the two provenance fields before you trust a link. They answer different questions, and conflating them is the classic mistake:

FieldMeaning
origin: "content"Extracted from a [[type::TARGET]] chip in the note body. Its lifecycle IS the body's — delete the chip and the link stops being live.
origin: "declared"Not attributable to a chip in the current body: a structural link (from a duplicate/move), one created explicitly, or an old link predating chip tracking (see caveat below). Editing the body never retires it, so a declared link that no longer holds can ONLY be removed with rel rm.
live: falseA content chip that is gone from the note's current version. This is the obsolete one. Hidden unless you pass --history.
isStale: trueCitation drift, NOT obsolescence. The link holds; the note it points at has simply advanced since it was cited (madeAgainstVersion < relatedNoteCurrentVersion). Often the most interesting signal in the graph — a claim resting on a source that moved. Worth re-reading, never worth auto-deleting.

Relationships are append-only: every save re-extracts the body's chips into fresh rows and the old ones become history rather than being deleted. The default read already hides superseded rows, so rel list reflects the CURRENT content — you do not need to filter it yourself.

Caveat on declared: chip tracking was added after the fact, so links created before it report declared even when they DID come from a body chip. Read declared as "not tracked to a chip", not as proof the link was never in the prose — the note body is the authority. This only ever makes a link too durable (it stays live), never too fragile, so declared is always safe to trust as "still asserted"; just don't infer from it that the prose never said so.

See how a found note connects (the editor header, for agents). When you land on a note via search semantic / notes list, the two affordances the editor shows at the top of the panel are both one call away — use them to orient before acting on the note:

cnotes rel list --note INSIGHT-1 --json     # Backlinks: .incoming = what links here
cnotes notes canvases INSIGHT-1 --json      # Canvases: where this note already lives

Reach for these to expand context around a search hit (walk .incoming to find who depends on a decision), to avoid duplicating a note that's already placed on the right canvas, or to gauge a note's importance (many backlinks / on the home canvas = load-bearing). cnotes schema --json carries the same pointers under its discovery key.

Interlinking Notes

When creating multiple related notes, interlink at two levels:

1. Relationship mentions inside note content — connects notes in the knowledge graph:

  • Use cnotes notes create with an array and @key placeholders for cross-references.
  • Example: [@B: The Problem](relationship:extends) in markdown.
  • The server resolves placeholders to real display IDs in one atomic transaction.

2. Canvas edges between nodes — visual connections on the canvas:

  • After adding nodes, connect them with cnotes canvas add-edge, using short verb labels ("triggers", "depends on").
  • Edges can join any node type, as long as both endpoints are on the same canvas.

Both levels are needed — edges are canvas-local, mentions are content-level. cnotes canvas read does return edges, but only the mention creates a real graph edge that rel list and search can see, so any relationship that matters must also exist as a mention in the note bodies (see hard rules 2 and 5 in canvas-elements.md).

Placeholder Syntax for cnotes notes create

Use [@key: Title](relationship:type) where key matches a note's key field in the --notes JSON:

This builds on [@B: The Problem](relationship:extends) and enables [@C: The Workflow](relationship:enables).

The relationship type is free-form, but reuse an existing type before inventing one — see Content via Markdown in notes.md for the reuse rule, why needless synonyms are harmful, and the built-in types to reach for first.

Match the type to your own verb. If your sentence says "blocks", the mention must say relationship:blocks — a verb narrated in prose around a references chip (or an untyped [[NOTE-12]]) is an edge the graph can't see: backlinks and rel list will file it as a plain citation. notes create and versions create detect this and return a mentionWarnings array (--json) plus a stderr note with the exact retyped mention; treat each warning as a retype instruction unless the flagged word genuinely isn't asserting a relationship. Writing the verb right before its chip is fine: "This reopens [[reopens::DECISION-6KW]]" renders as "This reopens → DECISION-6KW Title", because a chip drops its own label when the words just before it say the same thing (whitespace only between them, so not "reopens: …").

Dependencies (cnotes deps)

"When this note changes, check that dependent." A dependency is a depends-on relationship: the dependent (an FAQ, a landing page, a deck) depends on a basis (a behaviour, a decision, a claim). You declare one by writing the chip in the dependent's body, in a sentence that says why — the sentence is recorded as the reason:

No setup is needed because [[depends-on::NOTE-129]] creates the rules automatically.
cnotes deps list --canvas CANVAS-58      # grouped: changed (start here), suggested, unchecked, then checked
cnotes deps show <relationshipId>        # both notes now, and as they were when last checked
cnotes deps review <relationshipId> --outcome update_needed \
  --explanation "FAQ answer 3 still says no setup; the flow now needs one confirmation." \
  --propose "Change 'no setup needed' to 'one confirmation step'."

Everything deps review records is a SUGGESTION. It is attributed to you (and to the running operation when you are inside one) and waits for a person to confirm it in the app. It never edits a note or publishes anything. Outcomes: update_needed (dependent no longer matches), no_change (checked, still holds), updated (checked after the dependent was updated), uncertain (cannot tell from the evidence). Read deps show before you judge: a save names the versions you looked at and is refused if they changed since.

After you edit a note, run deps list on its canvas: the dependencies that now read changed are the ones your edit may have broken, and you are the one who knows what changed.

Reviews, comments and the shared inbox

Read this when asked to address comments, review a canvas, request a review, or see what teammates sent.

Comment or propose?

When you review a canvas or a note, sort each point by whether you can write the fix. A comment that says "this should say X" makes the person read it, agree, then write X themselves. A written change hands them the result with its reason, and they choose to keep it or not.

  • You can write the fix, and other people read the work: propose it. Open a draft and draft propose the note, or branch the board (drafts.md). Give the reason in --description or the run's prompt, the way you would have worded the comment.
  • You can write the fix, and only the user reads the work: say in your reply what you would change and why, then make the change when they say yes, inside an operations run so it stays undoable.
  • You cannot write it: comment. A doubt you cannot resolve ("this claim looks wrong and I can't say why"), a question only the person can answer, or a finding inside a review round. Never invent a fix to avoid a comment; a doubt that deserves an answer can become a Question note.
  • The user asked you to "leave comments": comment every point, fixes included. They chose the channel.

A person's comments run the other way: they are instructions for you. The change you make, or propose on shared work, is the answer to each one.

Canvas comments

# Comments — PRIVATE anchored comments on canvas nodes: the human→agent review
# queue. Visible only to their author + agents acting as them (never to other
# workspace members) UNTIL the author sends them as a review with
# `comments submit`, which hands that canvas's unsent comments to ONE person,
# the canvas owner. Excluded from digests, search, embeds and public
# shares. Process, not knowledge: open→resolved lifecycle; a comment that is
# still true after it resolves should be promoted to a typed note. When asked
# to "address the comments" on a canvas: `comments list`, act on each item
# inside an operations run, then `comments resolve` each — the active run is
# recorded automatically as what addressed it. `--node` takes a raw node id,
# a NOTE display id, or a SECTION display id. `isStale: true` on a listed
# comment = the target note changed after the comment was written (re-read,
# don't skip). `targetMissing: true` = the anchor node was deleted; the
# comment still stands — resolve or rm it explicitly. `list` returns at most
# 100 comments per status; `truncated: true` in --json means more exist
# (resolve some, then list again). Use `comments add` (author defaults to
# agent from the CLI) to leave anchored self-notes for future sessions
# ("don't move SECTION-42 — the filmstrip references it by position").
# OMIT the canvas id to read your open queue across the WHOLE workspace — the
# only entry point when the user names a NOTE and not a canvas ("I left you a
# review comment on STAGE-6H1"), because a note does not resolve to the canvas
# it sits on. Every row is stamped with `canvasId`/`canvasDisplayId`, so you can
# resolve it without a second lookup. Workspace-wide reads are OPEN comments
# only (--status errors rather than quietly narrowing) and cap at 200, with
# `totalOpen` in --json capped the same way — read it as a floor, not a census,
# whenever `truncated` is true.
cnotes canvas comments list [canvasId] [--status open|resolved|all] [--received]
cnotes canvas comments add <canvasId> --node <nodeRef> --text "<content>" [--author human|agent]
cnotes canvas comments resolve <canvasId> --id <annotationId>
cnotes canvas comments reopen <canvasId> --id <annotationId>   # Undo a resolve; the comment rejoins the open queue
cnotes canvas comments rm <canvasId> --id <annotationId>
# `--received` switches to a DIFFERENT queue: reviews other members submitted TO
# you (each row carries `submittedByName`), on one canvas or across the
# workspace depending on whether you passed a canvas id. When the user says
# "address the comments Will sent me", you MUST pass --received — the default
# queue is your own comments and will look empty. Resolve received comments the
# same way; you may never delete one (only its author can).
# Reviewing SOMEONE ELSE'S canvas: comment freely (view-only members can), then
# submit once. Nothing you wrote is visible to anyone until you do, and the
# owner then sees each comment anchored to the card it is about, grouped under
# your name in their review queue. Idempotent — a second submit sends only what
# you have written since. There is no unsend. Reviews you RECEIVE arrive in the
# same `comments list` queue and are yours to resolve (never to delete).
cnotes canvas comments submit <canvasId>

Asks (cnotes asks)

# Asks waiting on the user: direct canvas/note shares, reviews and comments
# sent to them, and draft rounds they were asked to decide. An INBOX, not
# history. Items persist until acted on, they never age out of a window.
# `cnotes shared` is the same command.
cnotes asks [--from <person>] [--limit <n>]

# Examples
cnotes asks                            # everything waiting on the user
cnotes asks --from ben                 # just what Ben pointed at them
cnotes asks --quiet                    # target refs only (CANVAS-12, ...)
# --from takes a name (case-insensitive, unique substring ok); if it matches
# more than one member the error lists the candidates — use a fuller name.
# --json rows: { kind: "share"|"review"|"ask", senderName, targetKind,
#   displayId, title, ts, commentCount? (reviews), ask? (asks) }

Review requests (cnotes review / cnotes reviews)

Official review requests, both legs. The canvas IS the request, and you compose it for the person who reads it, not as a link to what you made. Never simulate a review request with private canvas comments: comments are drafts for a round, not the round itself.

Compose the receiver's canvas

Every ask reads the same way, so a reviewer who has opened three knows where to look on the fourth. Lay it out in this order:

  1. What this is. One sentence naming the board or the version under review. It opens --brief.
  2. What it decides, and by when. The decision the answers feed and the date it is needed. Also in --brief.
  3. What you need from them. One Question note per thing you need answered, each placed on the canvas and each with a ## Suggested answer section holding the answer you expect and why, in a sentence or two. The reviewer confirms it in one tap or corrects it in a sentence, instead of writing an answer from nothing. Ask as many as the decision needs; there is no cap. When you have no view, say what you would do and where your confidence runs out: a blank draft hands the work back to the reader.
  4. What you are not asking about. One sentence at the end of --brief, so the reviewer does not review the wrong thing.
  5. The context. Reuse the notes from your research and portal back to the board it came from. Do not write new notes to carry context.

review request warns on stderr (and in warnings with --json) about each question with no suggested answer, and about a board with no questions at all. Fix the notes rather than ignore the warning.

A Question note written for an ask looks like this:

# Is 25 seats the right cap per workspace?

Gates the pricing page copy on [CANVAS-81: Pricing](relationship:references).

## Suggested answer

Yes. Every pilot team stayed under 20 seats, and the first 30-seat prospect
is a year out. Revisit when a team passes 25.
# REQUESTER LEG — create, monitor, manage.
# --reviewer me = SELF-REVIEW, actionable immediately, no invitation step.
# --reviewer <memberUserId> assigns a workspace member the same way.
# --reviewer <email> (or --email) binds an outside reviewer; they accept via link.
# Omit all three for a link any workspace member can claim.
cnotes review request CANVAS-81 \
  --reviewer me \
  --target SECTION-135 \
  --brief "Approve the Gong-faithful customer target" \
  --acceptance "Confirm the document boundary" \
  --acceptance "Confirm source fidelity" \
  --source ATTENTION-6J1 \
  --idempotency-key "attention-6j1-second-iteration" --json
# → { requestId, url, … }. url is the CANONICAL human review link, returned by
# the server — never assemble one yourself. requestId is the automation handle.
# Same idempotency key ⇒ the same request comes back (alreadyExists: true),
# never a sibling. --questions '[{"question":"…","suggestedAnswer":"…"}]'
# mints new Question notes, each with its Suggested answer section, when the
# canvas doesn't already carry them; --brief falls back to the canvas goal.

cnotes review list CANVAS-81                # asks out on a canvas + status
cnotes review get <requestId|url> --json    # the full envelope (see below)
cnotes review send-again <requestId> --email ana@example.com
cnotes review revoke <requestId>

# REVIEWER LEG — receive, inspect, complete.
cnotes review list --assigned-to me         # the worklist: reviews waiting on you
cnotes review list --assigned-to me --status all
cnotes review get <requestId> --json        # brief, acceptanceCriteria, targetRef,
                                            # slate (each question with its
                                            # suggestedAnswer), findings,
                                            # canvasChanged, allowedActions, url
# Answer each question: confirm the suggested answer when it is right, write
# your own when it is wrong or missing. Answering again before you complete
# replaces your answer. --confirm-all confirms every open question that has a
# suggested answer. Answers are delivered by `complete`, with your comments.
cnotes review answer <requestId> --question QUESTION-12 --confirm
cnotes review answer <requestId> --question QUESTION-14 --text "No, Clio renews in January"
cnotes review answer <requestId> --confirm-all
cnotes review complete <requestId> \
  --outcome approved \
  --rationale "Matches the source and preserves the agreed boundary" \
  --idempotency-key "review-run-123" --json
cnotes review complete <requestId> --outcome changes-requested \
  --rationale "The boundary drifts from Gong" \
  --criteria '[{"criterion":"Document boundary","met":false,"note":"two extra lines"}]'
# Exactly ONE outcome per round: approved | changes-requested, rationale
# mandatory. Findings you added against the request (canvas comments bound to
# it) are delivered in the same act. If the envelope says canvasChanged, the
# board moved since the ask — re-read it, then add --accept-stale; completion
# never silently applies to a moved version. Same idempotency key ⇒ the
# recorded outcome comes back instead of a conflict.
# If you CANNOT judge it (missing evidence/access), never fake a completion —
# hand it back with the structured blocker:
cnotes review return <requestId> --reason "I can't open the Gong recording"
# `get` accepts a request id, a raw token, or a full /review/<token> URL, so a
# pasted review link is enough to pick the work up here.