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
- Confirm the workspace.
cnotes workspace currentnames it and lists its relationship types.-w <id>targets another one for a single command. - 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>"andcnotes canvas search "<topic>".cnotes notes get <id> --expandloads 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. - 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 carriestypeRubrics, 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);-qprints only the run id, with the proposals on stderr. One canvas per run: end it and begin another before writing to a second canvas. - 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. - 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. - Grade what you wrote.
cnotes quality <displayIds...> --fail-below 4, then revise weak notes withcnotes versions create <id> --description "<what changed>" --markdown-file <path>. - Lay out the canvas.
cnotes canvas place <canvasId> --spec layout.jsonpacks 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 --spectakes 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: onelargeuntinted text card at the top-left, above everything, holding# <the goal's result>in under 50 characters, with the four-line banner under it atmedium. 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. - 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> --expandon what you wrote, read as the next session will, without this conversation. - Close the run.
cnotes operations end. - 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.
- Read, in parallel:
cnotes canvas read <canvasId>(orcnotes canvas read-section SECTION-nwhen the request names one), pluscnotes types show <Type>for each type you will write. - 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 nooperations 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:Put the new notes in the spec's{"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"}]}notesarray (thenotes createshape) and place each with{"kind":"item","type":"note","noteId":"@<key>"}; edges and anchors say@<key>too. Add--into SECTION-nto append to an existing frame. Each result comes back undernotes, 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". - 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
- The exit code is the verdict.
0is success. Never decide success from the body, and never re-run a write because a key looked empty: a secondnotes createduplicates the batch. - 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).
- Name notes with titled, typed mentions.
[NOTE-12: Title](relationship:<verb>), with the verb your sentence uses (blocks,depends-on,answered-by). A bareNOTE-12in prose creates no link and is a defect, in note bodies and on canvas cards.referencesis for pure citation only. Reuse the workspace's types (cnotes schemaranks them by use;cnotes rel typeslists all) before coining one. - 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.
- Place declaratively, edit in place. Use
canvas place/canvas arrange, never hand-computed x/y. Add to a section withplace --into SECTION-n(or--sectionon a single add); tidy one witharrange --section SECTION-n. Change a text card withcanvas update-textand a frame withcanvas update-section. Deleting and re-adding mints new ids and drops edges and[[SECTION-n]]mentions. - Content changes are versions.
cnotes versions createchanges a note's body (the# h1is its title); fetch the current body first and edit that, since people edit between sessions.notes updatechanges metadata only (type, tags, pin, archive). - Inside a draft, propose and stop. Shared notes change through
cnotes draft propose; the hand-over iscnotes draft ask, then stop. Never merge or approve work you built. Detail: references/drafts.md. - Ask before spending something the user cannot get back:
cnotes upgradeuses the workspace's one-time trial,cnotes feedback submitfiles a report in their name, andcnotes types dimension backfill --yesspends one model call per existing note. - 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.
- 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.
| Flag | Effect |
|---|---|
--json | Raw JSON. The default whenever stdout is not a terminal. |
-q, --quiet | One handle per line: displayId, else name, else id. |
--fields a,b | Keep 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). |
| Exit | Meaning | Next move |
|---|---|---|
0 | success | |
2 | auth required | cnotes auth status (it exits 2 when signed out), then cnotes auth login |
3 | validation | fix the input the error names (details.items lists each bad batch item) |
4 | not found | the hint names the list command to run |
5 | conflict | re-read and retry; an open run blocks operations begin |
6 | rate limited | back off and retry |
7 | permission denied | cnotes workspace current |
8 | subscription 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/searchemit a bare array (one note is.[0]).notes createreturns{ results, relationshipsCreated, rubrics?, hints? }; read new notes fromresults.canvas createreturns{ canvas: { id, displayId, url } }withid,displayIdandurlmirrored at the top level. - Errors go to stderr as
{ error, code, exitCode, hint }. Thehintnames the command that fixes it; read it before retrying. notes listsays 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; readShowing N of Mas 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 iscnotes query(references/more-commands.md).cnotes auth statusandcnotes whoamiexit 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
jqneeded.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"
Surfacing the canvas link
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 are | Open |
|---|---|
| Writing any prose a person will read | references/humanizer.md |
| Creating, reading or revising notes; types, rubrics and dimensions; markdown rules | references/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 tests | references/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, digest | references/canvas.md |
| Running two agents at once, bracketing canvas edits, verifying attribution, reading the timeline | references/operations.md |
| Reworking notes or a board other people read, or asked to draft | references/drafts.md |
| Another agent works in the same workspace: handing work over, reviewing another agent's proposal, overlaps, settling it, when to stop | references/agents-together.md |
| Reviewing (propose the fix, or comment), addressing comments, requesting or answering a review, checking what teammates shared | references/reviews-and-comments.md |
Hitting exit 8, or asked for another workspace | references/billing.md |
| Planning focus blocks, handling images, setup, theme, config, or reporting a CLI problem | references/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 asCNOTES_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 withCNOTES_PROPOSAL=<id>(ornoneto 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>"orcnotes 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 askordraft 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.
Paid actions and workspaces
Read this when a write fails with exit 8 (subscription_required) or the user wants another workspace.
Paid actions (writes need Pro)
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:
| Element | What it is | CLI |
|---|---|---|
| note | A reusable knowledge object: typed, versioned, searchable, with a display ID other notes can mention | add-node, place type:"note" |
| text | Free-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" |
| list | A container whose content is its MEMBER NOTES; the description is a one-line frame | add-list |
| section | A named region frame (Miro-style). Mints SECTION-<n>, mention-addressable | add-section |
| canvas | A portal to another canvas | add-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 have | Use | NOT |
|---|---|---|
| A claim, fact, decision, risk, question, verdict, score, spec, or example worth keeping | A 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 frame | bullets 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 bodies | a 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 case | a tinted card, or a section frame for something that isn't a region |
| The board's goal, readable at zoom-to-fit | one large untinted text hero at the top-left, # <the goal's result>, shaped by rule 7 | a medium heading standing in for it, the canvas name repeated, a tinted hero |
| Orientation: what happened, what the board is for, how to read it | one 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 presence | a 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-contained | the 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 set | a caption note per tile, or rationale in the tiles |
| "A connects to B" between two notes | a labeled edge (short verb phrase), notes only | prose explaining the connection |
| The exact line from a transcript or note that carries a point | a 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 Insight | pasting the words into a new note or a text element, where they drift from the source |
| A pointer to another canvas | a portal — add-link on the CLI, {"type": "canvas", "linkedCanvasId": ...} in place specs, returned as canvasLinkNodes by canvas get | a 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 reverse | narration on the shared canvas |
Hard rules (each one has burned a real canvas)
-
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.)
-
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). -
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. -
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.
-
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 readreturns 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 inrel 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. -
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:Role Hex Use 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 (
#c2410clands 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.
-
The hero and the banner open every canvas. A cold reader sees them first, often only them.
The hero is one
largeuntinted 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
mediumuntinted text card, no heading (the hero is its heading). Write one line each, in this order, no paragraphs:- 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."
- What it's for. The decision or artifact the board produces.
- Who acts. Who decides or builds, and where the answer goes (a mention chip, rule 2).
- 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.
-
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-quoteeach 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
largequote per board: the line everything rests on.mediumfor one to three supporting lines.smallfor evidence beside the card it backs. Never two large quotes; the hero is the only other large card. - Style follows the job.
pullfor a line that stands alone.highlightwhen the words need their sentence around them.evidencefor data or an excerpt backing a claim next to it. - Colour follows rule 6, by meaning.
insightfor the hero line,evidencefor customer words and data,warningfor the caveat that could sink the plan,peoplefor 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 createwith typeQuote, the words in a blockquote, then afrom <who>, <where>, <date>line. Place it like any note and style it withstyle-quote.
- Size is hierarchy (rule 9). One
-
Three sizes make three layers. A text card's
sizesets 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.Size Holds How 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 quotes a few smallprojections, annotations and lane keys beside the card they speak for as many as the cards need Pick the size by reach: a band heading at
largecompetes with the hero, and an annotation atmediumoutshouts 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 readreturns 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 */ }directionis exactly one of"right"|"left"|"above"|"below"— NOTtop/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> --stopdoes 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:
@<key>— any item placed in the same call that carries a matchingkey- Original
noteIdof a note placed in this call (e.g."NOTE-7") — nokeyneeded - 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,
placeone 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 tocanvas arrange --specwith"type": "existing"leaves (older heading cards are"nodeType": "text"), anoriginat the board's top-left and"placement": "exact". Run it with--dry-runfirst and check that thebboxbottom 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
- Write the layout doc to a temp file (
/tmp/layout.json). - Run
cnotes canvas place <canvasId> --spec /tmp/layout.json --json. - The response includes
items[](with resolvedpositionX/Y,width,height,id, and yourkey),edges[](with resolved edge ids), andbbox(the bounding box of the whole layout).
Constraints
- Anchors take zero space in their parent — when nested inside a
stackorgrid, 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@keyrefs 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 nocolorHex. 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
-
Say why when you open it.
--intentis 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-12names the shared note that asked (the Question, the Problem, the note holding the request). Both also opendraft statuswhen a later session checks the draft out, so the work keeps its direction. -
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. -
Change a shared note by proposing it, never live. Read it (
cnotes notes get DECISION-12works from inside the draft), write the complete new body, thencnotes draft propose DECISION-12 --markdown-file new.md --description "<what changed>". An ordinarycnotes 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: truein--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-outinstead. -
Run
cnotes draft statusuntil 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. -
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
--intentfrom 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 measks 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 mergeandcnotes review complete --outcome approvedon 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. -
When they send it back,
cnotes draft statusopens with their line ("Changes requested by <name>: ..."). Fix it in the draft, run status until no check blocks, thendraft askagain and stop. -
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 statusanddraft listopen 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, thendraft askagain and stop. A live save of a note with a proposed new text prints that the save puts the proposal out of date. -
When another proposal rewords the same note, you are told wherever you look:
draft askprints "Overlaps: PROPOSAL-7 "Team price" also rewords DECISION-1, built by you via Codex (Slack)." on stderr,draft statuslists it as aproposal-overlapwarning, andoperations beginlists 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 checkoutandworkspace currentprint it: nobody asked yet, the changes a person asked for, or a named reviewer. When the next move is yours, it ends with thedraft askto run.--jsoncarries it asdecision.line, withdecision.state(awaiting_person,changes_requested,waiting_on_reviewer). - Ids survive. A draft mints display ids from the shared workspace's
sequence, so
DECISION-6K9written in a draft is stillDECISION-6K9once it lands. In the rare clash the id is re-minted, every mention is rewritten, anddraft statuslists the change (merge.renamedin--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 andCNOTES_PROPOSAL=noneworks 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;-wnaming the draft's own shared workspace still goes into the draft. Type setup is live work:types updaterefuses inside a draft, because a merge carries notes, never type changes. - After it lands,
draft statussays who approved it and which ids were renamed, and the CLI stops sending commands into it (so doesworkspace 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 theUndo:line (undoUrlin--json). - When the live board moved under a branch, start over rather than
merge. Settle one or two conflicts on the copy as
draft statussays. When the board changed under much of your work, or a conflict needs the board's owner to act, runcnotes 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, thendraft statusand ask again. Your draft's notes and proposed rewordings stay; their cards on the copy go, so place them again. The list stays indraft statusuntil 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 beginwhile 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
| Mode | What you do |
|---|---|
| Draft cleanly | Author new prose already free of slop — the audit is baked into the first draft, not bolted on. |
| Audit only | Report defects with locations and no edits. Hand the nuanced findings to the user. |
| Rewrite with preservation | Fix the flagged prose while the preservation gate holds every fact, number, ID, and link fixed. |
| Match a voice card | Apply 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)
- Draft for meaning and the note-type rubric (
cnotes types show <Type>). Own the outline yourself before filling it in — see Structural slop. - Run the anti-slop audit against the kill-list and moves below.
- 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.
- Run the preservation check — verify nothing in the preservation gate changed.
- Save with a
# h1title and readable relationship-mention titles.
Existing notes and canvases (audit → select → version → verify)
- Read the canvas digest (
cnotes canvas digest <id>) and the note bodies. - Audit only. Identify notes with clear problems; leave clean notes alone.
- Create a new version (
cnotes versions create) only for the selected notes, with a change description saying what you fixed. - Fix affected canvas text cards in place with
cnotes canvas update-text(text cards have no version history). - 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 move | Do instead |
|---|---|
| Em dashes in user-facing prose | rewrite 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 source | pair every number with its baseline and where it came from |
| Both-sides hedge-seesaw presented as a view | commit to a stance and name its cost |
| Exclamation points in feature copy | a 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:
- 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.
- Missing falsifier — on an empirical or decision claim, no statable "this is wrong if X by date D."
- 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 areferenceschip 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
# h1heading — it becomes the title. - Relationship mentions include a readable title:
[NOTE-123: Title](relationship:type), never bareNOTE-123. - Relationship type matches the narrated verb — prose "Blocks X" around a
references/untyped chip is a graph bug, not a style choice: retype torelationship: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--promptis 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  syntax
# Read an image that's INSIDE a note. A note's content prints images as
#  — 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
answeredwhen a note links it withanswers(or the Question links its answer withanswered-by); any note issupersededBywhen a newer note sayssupersedes. 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
--typeparameter 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, runcnotes types show <Type>(or readtypeRubricsfromcnotes operations begin --json/validationPromptfromcnotes 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 --jsonalso puts the rubric on each.results[]item (type+rubric) and as a top-levelrubricsarray. Do not cut the output down to onlydisplayIduntil you have checked.results[].rubric. After the write, you grade:cnotes qualityhands backgradeRequest(rubric + content + "Grade this against the rubric") and spends no credits. Record the grade withcnotes quality record. A Decision rubric that declaresRequired 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" → prefixIDEA). - 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
markdownonly for short, single-line content. - Always start markdown content with a
# h1heading. 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:## h2for major sections,### h3for 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, …);referencesis 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 createandversions createdetect bare IDs that name real notes and return them inplainTextIdWarnings(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
quotesmention pinned to it. Read the version number and its time fromcnotes versions list <id>; the words must be that version's, verbatim:It is a hard copy: the version never changes, so the block never goes stale, and saving it records a> We lose them at the invite step > > — from [[quotes::TRANSCRIPT-4@v5]] v5, 12 Oct 2026, 4:55 pmquotesrelationship pinned to v5. Say what the quote means in your own sentences around it. - Reuse an existing relationship type before inventing one.
cnotes schemalists the workspace's slugs most-linked first (cnotes rel typeslists 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 (supportvssupports,relates-tovsrelated-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, anddepends-onall resolve to the same type. Whencnotes notes createdoes 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 notesreferences— 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 contentextends— adds depth or detail to another note's topicinvalidates/supersedes— supersedes or contradicts another noterelated-to— loose thematic connectionduplicates— 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
# h1heading 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
| Action | Wrap? |
|---|---|
Reading: digest, get, list, activity, timeline, search, rel list | No — 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 change | Yes — 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 task | Yes — 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>→ confirmsaffectedNoteCountreflects 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:
| Field | Meaning |
|---|---|
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: false | A content chip that is gone from the note's current version. This is the obsolete one. Hidden unless you pass --history. |
isStale: true | Citation 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 createwith an array and@keyplaceholders 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 proposethe note, or branch the board (drafts.md). Give the reason in--descriptionor 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:
- What this is. One sentence naming the board or the version under
review. It opens
--brief. - What it decides, and by when. The decision the answers feed and the
date it is needed. Also in
--brief. - What you need from them. One Question note per thing you need
answered, each placed on the canvas and each with a
## Suggested answersection 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. - What you are not asking about. One sentence at the end of
--brief, so the reviewer does not review the wrong thing. - 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.