---
name: cnotes
description: CreatorNotes CLI for the terminal. The command is `cnotes`. Use when the user mentions CreatorNotes or the cnotes CLI, asks to use this CLI skill, or wants to create, read, update, search, summarize, organize, or visualize notes, canvases, or transcripts in a CreatorNotes workspace. Covers cnotes notes, cnotes canvas, cnotes timeline, cnotes shared (what teammates shared with you), cnotes review (official review requests: request, receive, complete), cnotes focus (plan flow-state blocks landed on a target card), cnotes search, cnotes versions, relationships, and operations. Prefer the cnotes CLI, verify the active workspace with cnotes workspace current, use --json when parsing output, and wrap write operations in cnotes operations begin and cnotes operations end.
metadata:
  short-description: Work with CreatorNotes via the cnotes CLI
---

# CreatorNotes CLI (`cnotes`)

A command-line interface for CreatorNotes — create notes, build canvases, search knowledge, and manage workspaces from the terminal.

## Writing quality — the humanizer (read the reference before authoring prose)

Whenever a task **creates or edits audience-facing prose** that will be saved to
CreatorNotes — a note body, a pre-read, a strategy or workshop note, a public
draft, a Slack message, or a canvas orientation banner — **read
[`references/humanizer.md`](references/humanizer.md) first**, then draft, audit,
and save through it. It removes generic model habits (the kill-list, fake
contrasts, forced triads) while a preservation gate holds every number, date, ID,
link, and mention title fixed, so improving prose never corrupts meaning.

**Skip it** — do not humanize — for raw capture and structured data: transcripts,
voice memos, customer quotes, source evidence, schemas, tables, and metric-heavy
reference material. Store those unchanged and humanize only the derived artifact.

**The user's words override.** "Save this as raw capture", "audit only, change
nothing", "preserve timings and links", "use the neutral / company voice", and
"rewrite in my personal voice" each select a specific mode described in the
reference. Loading the reference costs no extra context on non-writing tasks —
pull it in only when the work is prose.

## Getting Started

### Install

```bash
npm install -g @creator-notes/cnotes
```

### Authenticate

```bash
cnotes auth login           # Opens browser for OAuth
cnotes auth login --token <key>  # Direct API key
cnotes auth status          # Check current session
```

### Select a Workspace

```bash
cnotes workspace list
cnotes workspace select <id>
cnotes workspace current    # Verify active workspace + see its relationship types
```

`cnotes workspace current` also lists the relationship types already defined in the
workspace — reuse those labels when cross-linking notes rather than coining new ones
(see [Relationship mentions](#content-via-markdown)).

### 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.

```bash
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.

```bash
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.

## Command Reference

### Global Flags (apply to ALL commands)
```
--json           Output raw JSON (best for piping / parsing)
-q, --quiet      Minimal output (IDs only — useful for scripting)
-w, --workspace <id>   Override active workspace
--server <url>         Override server URL (default: https://creatornotes.app)
```

### Reading output (start here)

`cnotes` follows one output contract across every command. Learn it once and you never have to guess.

1. **Success is the exit code, not anything in the body.** Exit `0` means the command succeeded. Never decide success by looking for an `ok` field or a particular key. The body is data; the exit code is the verdict. Failures use a typed table so you can branch on the class of error:

   | exit | meaning | typical fix |
   |------|---------|-------------|
   | `0`  | success | n/a |
   | `2`  | auth required | `cnotes auth login` |
   | `3`  | validation (bad input) | fix the input shown in the error |
   | `4`  | not found | the `hint` names the list command to run |
   | `5`  | conflict (already exists / state collision) | re-fetch and retry |
   | `6`  | rate limited | back off and retry |
   | `7`  | permission denied | `cnotes workspace current` |
   | `8`  | subscription required (free workspace, write blocked) | offer to run `cnotes upgrade` (free trial, no card) — see [Paid actions](#paid-actions-writes-need-pro) |

   Exit `8` is specific: CLI/API **writes** require a Pro workspace, but **reads work on any plan**. The `--json` error carries `serverCode: "subscription_required"`, an `upgradeUrl` (the workspace's billing page), and a `hint`. **You are not stuck here.** If the caller owns the workspace and hasn't used its one-time trial, `cnotes upgrade` unlocks writes in a single command — free, no credit card, no browser. Ask the user first (it burns the one-time trial), run it, then retry the write. Only when the trial is already spent does this become a hand-off to the `upgradeUrl`. Full playbook, including the not-the-owner case: [Paid actions](#paid-actions-writes-need-pro).

2. **Reads return their data directly; writes return an envelope.** `cnotes notes list/get/search --json` emit a bare array, so `… --json | jq '.[0].displayId'` works. `cnotes notes create --json` returns an object: `{ results, relationshipsCreated, relationshipTypesCreated, rubrics? }`. The created notes live under **`results`** (not `notes`, not `ok`). Each result carries `displayId`, `title`, `type`, and `rubric` (null when the type has no validationPrompt). Prefer `jq '.results[] | {displayId, type, rubric}'` — do not filter down to only `displayId` until you have checked the rubric; a sibling top-level `rubrics` array is easy to miss.

   **`cnotes canvas create --json`** returns a single-resource envelope: `{ canvas: { id, displayId, url, … } }`, with the same `id`, `displayId`, and `url` **also mirrored at the top level**. So `jq -r '.id'` and `jq -r '.canvas.id'` both resolve — capture the id with either. Simplest of all: `CANVAS=$(cnotes canvas create "<name>" -q)` prints the bare Convex id and nothing else (best for scripts). Do **not** assume `.id` on the raw *server* response — that top-level mirror is a CLI convenience; downstream `canvas place`/`bulk-add` want the Convex id you captured here.

3. **Errors hand you the next move.** A failed command prints `{ error, code, exitCode, hint }` to stderr in `--json` mode. The `hint` is actionable: a wrong workspace id returns a 404 with `resource: "workspace"` and the hint `Run \`cnotes workspace list\``, rather than an opaque 500. Read the `hint` before retrying.

### Notes (`cnotes notes` / `cnotes n`)
```bash
# List notes (default 20, excludes drafts)
cnotes notes list [--search <query>] [--type <type>] [--tags <csv>] [--pinned] [--limit <n>]

# 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

# 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.
cnotes notes get TRANSCRIPT-1 --content-only
cnotes notes get TRANSCRIPT-1 --content-only > /tmp/transcript.md   # then chunk the file
# Without --content-only, fetch JSON and pull `.content` yourself:
#   cnotes notes get TRANSCRIPT-1 --json | jq -r '.[0].content'
# (Reads are a bare array, so a single note is always at .[0].)

# 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

# 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?}.
# 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.
# Each .results[] item carries .type + .rubric — keep them when filtering JSON.

# 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`)
```bash
# List versions
cnotes versions list <noteId>

# Create a new version (updates note content)
cnotes versions create <noteId> --description "what changed" --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
```

### Canvas (`cnotes canvas` / `cnotes c`)
```bash
# 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.
# (You rarely need this for placement — `cnotes canvas place` already avoids overlaps.)

# 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>

# 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: never put an em-dash (—) or en-dash (–) in a canvas name. Use the interpunct
# · (U+00B7, MIDDLE DOT — a small mid-height dot, NOT a bullet •) as the separator,
# e.g. "Path to Revenue · Funding strategy", not "Path to Revenue — Funding strategy".
cnotes canvas create "<name>" [--goal "<text>"] [--audience "<text>"]   # prints Ref (CANVAS-N) + Link — surface them to the user (see "Surfacing the canvas link")
cnotes canvas update <canvasId> [--title "<text>"] [--goal "<text>"] [--audience "<text>"]
cnotes canvas delete <canvasId>
cnotes canvas set-as-home <canvasId>
cnotes canvas archive <canvasId>
cnotes canvas unarchive <canvasId>

# Add nodes to canvas — see "Canvas Elements — When to Use What" before choosing.
# Default to NOTES for content; text/list/section are presentation furniture.
cnotes canvas add-node <canvasId> --note <noteId> [--x <n>] [--y <n>]   # (despite the name, add-node adds NOTE nodes only)
# text = free-form rich text placed on the canvas (wire type: richtext). Markdown —
# headings, emphasis, images. Optional background tint (--color); DEFAULT IS NO BACKGROUND.
# A HEADING IS JUST `--content '# Q1 Goals'` with no tint — there is no separate heading
# element. CARD-sized framing (at most ~3 sentences): orientation banners, emphasis
# quoting a note, image tiles. Unversioned, unsearchable, no display ID (nothing can cite
# it) — never the sole home of a claim. When the card names a note or canvas, write a
# relationship mention with the REAL title, `[NOTE-12: Title](relationship:verb)`: it renders
# as a chip (verified 2026-09-05). A bare display id in card prose is a defect. The chip is
# presentational (no graph edge), so knowledge worth keeping = a typed note.
cnotes canvas add-text <canvasId> --content "<markdown>" [--size small|medium|large] [--color "<hex>"] [--x <n>] [--y <n>]  # inline (short only)
cnotes canvas add-text <canvasId> --content-file <path.md> [--size small|medium|large] [--color "<hex>"] [--x <n>] [--y <n>]
cnotes canvas add-richtext ...   # DEPRECATED alias of add-text (identical flags). Use add-text.
# THE BOX IS DERIVED, NOT CHOSEN. You pick x/y; the server picks the size. WIDTH is the
# --size preset: small 560px, medium 1120px, large 1680px. HEIGHT follows the content, and
# there is no in-place content update: replacing a text element = remove-node + add-text,
# and the new card is exactly as tall as its new content. 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.
# When it happens the response carries layoutWarning naming the run-over nodes (CLI also
# prints it to stderr): reflow what's below (bulk-move / arrange), or hand the whole block
# to `canvas place` and let the solver pack it, then re-check with canvas get. Never treat
# "write succeeded" as "layout is fine" after a size bump or a content change.
# A list item is a COLLECTION OF NOTES, not a text block. --description is a one-line FRAME;
# the content is the member notes you pass to --notes. If you have N distinct things
# (questions, risks, options), create N notes first — pick the right type, e.g. a Question
# note per open question — then group them: --notes ID1,ID2,ID3. Pasting a numbered/bulleted
# list into --description with no --notes is the wrong shape (you get a "0 items" container
# masquerading as prose). For a short card-sized framing blurb (at most ~3 sentences, no
# member notes) use add-text; a multi-item markdown block is N typed notes, never a text element
# (see "Canvas Elements — When to Use What").
cnotes canvas add-list <canvasId> --description "<markdown>" [--notes <id1,id2,...>] [--view list|grid] [--x <n>] [--y <n>]
cnotes canvas add-link <canvasId> --target <otherCanvasId> [--x <n>] [--y <n>]
# 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).
cnotes canvas add-section <canvasId> --name "<title>" --notes <id1,id2,...> [--columns <n>] [--color "<hex>"]
cnotes canvas add-section <canvasId> --name "<title>" [--x <n>] [--y <n>] [--width <n>] [--height <n>]   # empty frame

# 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]

# Place items using a declarative layout (NO x/y math — server packs them)
# This is the PREFERRED way to add multiple items. See "Canvas Layout" below.
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

# 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)
# 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]
# remove-node takes a RAW node id only — it finds the canvas from the id, so it has
# nothing to resolve "SECTION-7" against. For a display reference use bulk-remove,
# which is also fine with a single entry.
cnotes canvas remove-node <canvasId> --node <nodeId>

# 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.
# Wrap it (canvas agent-run wrap) so the removal is undoable — see the undo rule below.

# 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 nodes — edges can join ANY node type (note, richtext, list, canvas-link, legacy text),
# as long as both endpoints live on the same canvas.
# Labels are short verbs ("fixes", "grounds", "then"). `canvas read` DOES return edges now,
# but an edge is still CANVAS-LOCAL: it is not part of the note graph, so it never shows up in
# `rel list`, is not searchable, and means nothing away from this canvas. So when an edge
# encodes a relationship between NOTES, ALSO mirror it as a relationship mention in the note
# bodies — that is the copy that travels. Edges to text/list/canvas-link nodes are diagram
# decoration (labels, portals) and need no mention.
cnotes canvas add-edge <canvasId> --source <nodeId> --target <nodeId> [--label "<text>"]

# 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>

# 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

# 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`)
```bash
# 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 -q "auth decision" --since 30d      # search within a window
# --json returns { mode: "search", query, results: [{ displayId, versionNumber,
#   changeDescription, snippet, createdAt, score, ... }] }
```

### Shared with you (`cnotes shared`)
```bash
# What teammates shared with the user: direct canvas/note shares, reviews sent
# to them, and review asks they accepted but haven't finished. An INBOX, not
# history — items persist until acted on, they never age out of a window.
cnotes shared [--from <person>] [--limit <n>]

# Examples
cnotes shared                          # everything waiting on the user
cnotes shared --from ben               # just what Ben pointed at them
cnotes shared --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: assemble it
first (context notes reused from research, Question notes to answer, an
instruction text node, a portal back), then request the review of it. Never
simulate a review request with private canvas comments — comments are drafts
for a round, not the round itself.

```bash
# 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 '["…"]' still mints new Question notes 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, findings, canvasChanged,
                                            # allowedActions for YOU, url
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.
```

### 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.

```bash
# 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.

### Relationships (`cnotes relationships` / `cnotes rel`)
```bash
# 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 | jq '.incoming[].relatedNote.displayId'

# 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:

```bash
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.

### Search
`cnotes search semantic` searches NOTE content (vector + BM25); `cnotes canvas search`
searches CANVASES (name / themes / digest, semantic + lexical). They are separate corpora —
to cover both, search each.
```bash
cnotes search semantic "<query>" [--limit <n>] [--canvas <canvasId>]   # NOTES
cnotes canvas search "<query>" [--limit <n>]                            # CANVASES
```

### Types (`cnotes types` / `cnotes t`)
```bash
# 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>
```

### Files (`cnotes files` / `cnotes f`)
```bash
# Upload an image to the workspace (max 5MB, supports .jpg .jpeg .png .gif .webp)
cnotes files upload <path> [--markdown]   # --markdown outputs ![filename](url) syntax

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

### Init & MCP (`cnotes init` / `cnotes mcp`)
```bash
# Interactive setup wizard — auth, workspace, AI integrations, skill install
cnotes init

# Configure MCP server for AI tools (also handled by cnotes init)
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).

```bash
# 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
```bash
cnotes config get-server
cnotes config set-server <url>
```

### Feedback (`cnotes feedback`)
```bash
# 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" in Best Practices
# 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 claude-opus-4-8
echo "Loved the batch @key cross-links." | cnotes feedback submit \
  --severity praise --area cli/notes --title "Batch create is the right primitive" --body-stdin
```

## Surfacing the canvas link (do this after canvas work)

When you create a canvas — or finish placing/editing notes on one — **end your reply with a link to that canvas.** The user should never have to go hunt for what you just built. Lead with the canvas's **display id** (the memorable, immutable handle like `CANVAS-321` — the CreatorNotes equivalent of a Jira ticket id) as the *link text*:

> Built **[CANVAS-321 · Pricing model](https://creatornotes.app/<workspaceId>?canvasId=<canvasId>)** — 7 notes across 3 columns.

`cnotes canvas create` and `cnotes canvas get` print the two pieces ready to paste — copy them straight through, don't re-assemble:

```
Ref:  CANVAS-321                      ← the display id — use as the link TEXT
Link: https://…/<wsId>?canvasId=<id>  ← the href (uses the raw Convex id)
```

(With `--json`, `canvas create` adds the same link as `canvas.url`, and `canvas.displayId` is the ref.)

`cnotes canvas place`, `cnotes canvas bulk-add`, and `cnotes canvas add-node` also print a `Link:` line **when you pass the canvas's raw Convex id** (the `ID:` you captured from `canvas create` / `canvas get`). Pass a `CANVAS-12` display id to those write commands instead and the link is omitted — the display id doesn't resolve in `?canvasId` — so hold the Convex id when you intend to surface a link.

For any canvas you only *added to* (you already hold its id, so you didn't re-`get` it), build the link yourself in this exact shape:

```
<origin>/<workspaceId>?canvasId=<canvasId>
```

- `<origin>` is `https://creatornotes.app` for prod, or `http://localhost:3000` when the active session / `--server` points at local dev. Match whichever the CLI is currently talking to (read it from `cnotes config get-server` rather than guessing).
- `<workspaceId>` and `<canvasId>` are the Convex IDs returned by `cnotes workspace current` / `cnotes canvas get` / `cnotes canvas list --json`. Use raw IDs — do not slug-ify.
- To link a workspace without a specific canvas, omit the query: `<origin>/<workspaceId>`. The app will redirect to the last-visited or home canvas.

**Display id vs link — keep them in their lanes.** The display id (`CANVAS-321`) is the human handle you show as the link *text*; the `?canvasId=` value must be the raw **Convex id**. The web router resolves Convex ids, NOT display ids — `?canvasId=CANVAS-321` will not load. So: display id as the visible text, Convex id inside the URL.

**Do not invent path segments.** These shapes do NOT exist and will 404 or redirect somewhere wrong:

- `/workspace/<id>/canvas/<canvasId>` ← wrong, fabricated
- `/workspaces/<id>` ← wrong
- `/canvas/<canvasId>` ← wrong (there is a `/shared/<canvasId>` route, but only for public share links)

## Best Practices

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

### Note Types
- The `--type` parameter must reference an **existing supertag** in the workspace. Free-text type strings are rejected.
- **Pick the type, then read its rubric, then write to it — in that order.** Each type carries a `validationPrompt` (its quality rubric: what a good instance contains). Once you know the type, run `cnotes types show <Type>` (or read `typeRubrics` from `cnotes operations begin --json` / `validationPrompt` from `cnotes types list --json`) and author to that rubric so the note lands well-formed instead of getting nudged after save. Example: a Decision needs the alternatives weighed, the rationale, and a revisit trigger. `cnotes notes create --json` also puts the rubric on **each** `.results[]` item (`type` + `rubric`) and as a top-level `rubrics` array — revise in-loop if a note falls short. Do not `jq` down to only `displayId` until you have checked `.results[].rubric`.
- If the type you need doesn't exist, create it first: `cnotes types create "MyType" --display-name "My Type"`
- Type names are **case-sensitive** and use PascalCase (e.g., `Update`, `PainPoint`, `Meeting`).
- The display ID prefix is **auto-derived** from the display name: spaces become hyphens, all uppercase (e.g., display name "Pain Point" → prefix `PAIN-POINT`, display name "Idea" → prefix `IDEA`).
- Prefix is **immutable** once notes of that type exist. To change a note's type, use `cnotes notes retype <displayId> --type <NewType>`.

### Content via Markdown
- Markdown is the only content format on the wire. For multi-line content, write to a file and pass the path via `markdownFile`:
  ```bash
  cnotes notes create --notes '[{"key":"A","type":"Note","markdownFile":"./note.md"}]'
  ```
- Use inline `markdown` only for short, single-line content.
- **Always start markdown content with a `# h1` heading.** This becomes the note title. Follow proper markdown structure: `## h2` for major sections, `### h3` for subsections. Well-structured heading hierarchy is expected for all notes.
- **Relationship mentions in markdown**: Use `[NOTE-123: Title](relationship:<type>)` — plain text "NOTE-123" does NOT create clickable mentions, and the title-less form renders as "Untitled". Match `<type>` to the verb your prose uses (`blocks`, `depends-on`, …); `references` is for pure citations. The typed wiki form `[[blocks::NOTE-123]]` is equivalent — it's how note bodies serialize mentions back to you, and an untyped `[[NOTE-123]]` is a plain reference.
- **The classic miss: capturing an answer/outcome that lives in another note.** "The answer is the packaging decision in DECISION-24" saved as plain prose creates no chip, no relationship, no backlink — the graph never learns the question was answered. Write the mention with the verb as its type: `Answered by [DECISION-24: Title](relationship:answered-by)`. `notes create` and `versions create` detect bare IDs that name real notes and return them in `plainTextIdWarnings` (stderr + `--json`), each with a paste-ready rewrite carrying the note's actual title — treat every entry as a fix instruction (save a corrected version) unless the bare ID is deliberate prose.
- **Reuse an existing relationship type before inventing one.** Run `cnotes rel types` (or read the list printed by `cnotes workspace current`) and pick the closest existing label. The type slot is free-form, so an unknown label is auto-created as a custom workspace type rather than dropped — but that means a typo or a needless synonym (`support` vs `supports`, `relates-to` vs `related-to`) silently pollutes the workspace's type vocabulary forever. Only coin a new type when nothing existing fits.
- Names are normalized to lowercase + hyphens, so `Depends On`, `depends on`, and `depends-on` all resolve to the same type. When `cnotes notes create` does create new types, it reports them (`+ created N new relationship types: …`) — treat that line as a prompt to double-check you didn't mean an existing one.
- Common built-in types to reach for first:
  - `depends-on` / `blocks` — dependency ordering between notes
  - `references` — general "see also" link (default if omitted)
  - `implements` — a note that delivers on what another identifies (e.g., feature → requirement, solution → gap)
  - `derived-from` — a note created based on another's content
  - `extends` — adds depth or detail to another note's topic
  - `invalidates` / `supersedes` — supersedes or contradicts another note
  - `related-to` — loose thematic connection
  - `duplicates` — notes covering the same thing
- Simple mention shorthand: `[NOTE-123]` or `[NOTE-123: Title]` (defaults to "references")

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

### JSON Mode for Scripting
- Use `--json` to get raw JSON output for parsing with `jq` or piping between commands. See [Reading output](#reading-output-start-here) for the contract behind these shapes.
- Reads emit a bare array; writes emit an envelope. Read created notes from `results`, never from `notes`/`ok`:
  ```bash
  # Capture the display IDs a batch create produced
  IDS=$(cnotes notes create --json --notes '[…]' | jq -r '.results[].displayId')
  ```
- Treat a `0` exit code as the only success signal. Do not re-run a write because an expected key looked empty; re-running a successful `notes create` duplicates the batch.
- On validation failure with `--json`, the per-item errors come back in `details.items` (array of `{index, key?, field, message}`) so they can be parsed and surfaced precisely.
- Use `-q` (quiet) to get just the ID(s), useful for chaining:
  ```bash
  NOTE_ID=$(cnotes notes create --notes '{"key":"A","type":"Note","markdown":"# Quick note"}' -q)
  cnotes canvas add-node <canvasId> --note "$NOTE_ID"
  ```

### Canvas Orientation

**Always start canvas work with `cnotes canvas digest <canvasId> --json`.** The digest returns the narrative summary, key themes, and a per-note summary list covering every note on the canvas. Use this as your single source of context before making any changes — do not read each note individually unless you need full content.

**`--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 `jq -r '.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. Trigger a narrative refresh with `POST /api/canvas/<canvasId>/digest` only if you need the narrative itself.

### Canvas Elements — When to Use What

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 material, 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 |
| Prose telling the reader how to traverse THIS canvas | a **text** orientation banner (one per canvas or band; up to ~5 sentences) | a Guide note nobody needs off-canvas |
| 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 |
| 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)

1. **A canvas element must never be the sole home of a claim.** Verdict tallies, dates, scan provenance, and even a moat-defining sentence have each been authored as text elements — then cited downstream while being unversioned, unsearchable, and uncitable. If a text element says something true and useful, a note says it first; the text element may quote the note. (Canvas-specific wayfinding — how to read THIS canvas — is the one exemption; it has no off-canvas value.)
2. **Name notes and canvases from text elements with relationship mentions, never bare ids.** `[NOTE-12: Title](relationship:references)` inside a text element renders as a mention chip carrying the target's real title (the Text node registers the mention schema; verified live 2026-09-05, and a list description renders the same mention as a labelled span). "See RISK-64" as plain prose is a defect: the reader gets no title and no link. Pick the relationship type from the verb the sentence uses and reuse the workspace vocabulary. The chip is presentational: it creates no graph relationship, is not searchable, and is not versioned, so a relationship that must survive off the canvas still lives as a mention in a NOTE body (rule 5).
3. **A text element is capped at card size**: at most 3 sentences, one heading at most, never a list of distinct knowledge items (questions, risks, findings — those are N notes; a 3-line color legend or lane key is fine). The one exception is the orientation banner (table row 5, up to ~5 sentences). A heading-only text element (`--content '# Q1 Goals'`) is of course fine — that is the smallest legitimate use. The cap is physical as much as doctrinal: a text element renders its full content inline, so long blocks explode the canvas — one canvas hit 43,000px tall from eight text reference blocks and had to be rebuilt as notes.
4. **Detail's HOME is the note body; text detail cards are projections.** Write the full spec/content INTO each note's body first. A parallel text card under a note is allowed only as a projection — it condenses or quotes a note that already contains everything, and says so ("from STAGE-3"). A detail card whose content exists nowhere else is the anti-pattern: one teaching canvas carried its entire method in text cards over one-line stub notes, and an agent reading it back got seven stubs and no method.
5. **Edges can connect any node type** (note, text, list, canvas-link, section — plus legacy heading nodes), as long as both endpoints are on the same canvas; labels are short verb phrases ("fixes", "grounds", "depends on"). A section endpoint is addressed by the section node's Convex id (`sectionNodes[].id` in `canvas get --json` — `canvas read` shows only display ids), not its SECTION-n display id, and the edge anchors to the frame's border — use one when the relationship's endpoint is genuinely the whole region ("this cluster feeds that stage"), not as a shortcut for a note-to-note link. `canvas read` returns edges, so they are no longer invisible on read-back — but an edge is still CANVAS-LOCAL: it is not part of the note graph, never appears in `rel list`, is not searchable, and means nothing once you leave this canvas. So any relationship **between notes** that must survive off the canvas has to ALSO exist as a mention inside the note bodies. The mention is the copy that travels; the edge is the copy you can see. Edges to text/list/canvas-link nodes are diagram decoration (pointing a label at a cluster, wiring a portal) and carry no note-to-note relationship to mirror.
6. **Color is a ROLE or it is absent.** Default neutral — a text element has NO background unless you pass `--color`, and that is usually the right look (an untinted heading is just type on the canvas). When a tint does mean something, pick from the six role hexes and reuse ONE role per meaning, 2–4 roles per canvas max:

   | Role | Hex | Use for |
   |---|---|---|
   | Evidence | `#3B82F6` | input, quote, data |
   | Commitment | `#22C55E` | decided or owned work |
   | Insight | `#EC4899` | the aha — 1–2 per canvas |
   | Warning | `#F59E0B` | caveat, risk |
   | Danger | `#EF4444` | blocker |
   | People | `#8B5CF6` | person, stakeholder |

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

#### Two completion tests

Run both 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 section labels, orientation banners, projections, and layout are for.

#### 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 material): one 5,000-char text element holding a "deep scan writeup", a text scoreboard holding the verdict tally, and a few notes for leftovers. Search finds nothing, versions record nothing, nothing can cite or link any of it — and the headline verdict goes stale the first time someone edits the card.

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

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

- Mentions inside text elements and list descriptions are presentational chips only: no graph relationship, no search presence, no versions (motivates rule 1 and the mirror-as-mentions half of rule 5).
- `canvas read` returns sections, edges and portals, but list containers still read back as their one-line frame only. An edge read back is still canvas-local and ungraphed (motivates rule 5's mirror-as-mentions requirement).
- Agents have no update verb for **text or list** elements — to change one, remove and re-add it (one more reason content that evolves belongs in notes, which have versions). Section frames are the exception: `canvas update-section` edits a frame in place.

### Wrapping batch work in an operation (ENFORCED for `notes create`)

When an AI agent (including you, Claude) 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 note creation.** `cnotes notes create` (the batch-create endpoint) is rejected with `operation_required` (HTTP 422) unless a run is active — the server refuses to create notes outside an operation. 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 | **No** — single metadata edits don't go through the guarded create path |
| 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 |

#### 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.

```bash
# 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.
cnotes operations begin --prompt "Reorganize 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' --x 0 --y 0
cnotes canvas place <canvasId> --spec /tmp/q1.json
cnotes canvas add-edge <canvasId> --source <a> --target <b> --label "depends on"

# 3. 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 `CURSOR_CONVERSATION_ID` (`bc-…`). 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, end it first or pass --force" message). The server-side reaper will eventually mark a stale `running` row as `abandoned` after about an hour, but that's a safety net, not the intended flow.

**If something goes wrong mid-batch** and you can't recover: `cnotes operations end --force` (on a future begin) ends the prior run server-side and starts a fresh one.

#### Writing a good `--prompt`

The prompt is the **intent** the user sees on the timeline row. Use the user's own language where you can:

- "Build a fan-out under Goals heading with Q1 milestones"
- "Reorganize the canvas into a SWOT layout"
- "Add risks list and connect to the parent goal"

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

#### Verifying it worked

After your run, the user (or you) can:
- Look at the **timeline panel** in the workspace → the run appears as one row with a bot icon, your prompt as the headline, and a "N notes" count
- Expand the row → each member note appears nested
- Run `cnotes operations get <agentRunId>` → confirms `affectedNoteCount` reflects everything the run touched

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

### Canvas Layout

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

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

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

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

#### Leaf item shapes

```json
{ "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.

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. `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:

```json
{
  "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`:

```bash
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`).

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 — When to Use What".

`list.description` is a one-line **frame**, never the content itself — the content is `noteIds` (the member notes). For N distinct things, create N notes (right type per item, e.g. `Question` for open questions) and list their ids. A `list` with a multi-item description and an empty `noteIds` is the wrong shape — use a card-sized **text** element (`richtext`, at most ~3 sentences) only for a short framing block with no members; anything longer, or any set of distinct items, is N notes.

#### Edges

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

1. **`@<key>`** — any item placed in the same call that carries a matching `key`
2. **Original `noteId`** of a note placed in this call (e.g. `"NOTE-7"`) — no `key` needed
3. **Display id or convex id** of a node already on the canvas

```json
"edges": [
  { "from": "@a", "to": "@b", "label": "depends on" },
  { "from": "NOTE-7", "to": "@a" }
]
```

#### Examples

**Fan-out under an orientation banner** (the failure case the agent kept getting wrong):
```json
{
  "root": {
    "kind": "stack", "axis": "vertical", "gap": "medium",
    "items": [
      { "kind": "item", "type": "richtext",
        "content": "# Goals\nWhat we're committing to this quarter.",
        "size": "medium" },
      { "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**:
```json
{
  "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**:
```json
{
  "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**:
```json
{
  "root": {
    "kind": "stack", "axis": "vertical", "gap": "medium", "align": "center",
    "items": [
      { "kind": "item", "type": "note", "noteId": "A", "key": "root" },
      { "kind": "stack", "axis": "horizontal", "gap": "medium", "items": [
        { "kind": "item", "type": "note", "noteId": "B", "key": "left" },
        { "kind": "item", "type": "note", "noteId": "C", "key": "right" }
      ] },
      { "kind": "item", "type": "note", "noteId": "D", "key": "tail" }
    ]
  },
  "edges": [
    { "from": "@root", "to": "@left" },
    { "from": "@root", "to": "@right" },
    { "from": "@left", "to": "@tail" },
    { "from": "@right", "to": "@tail" }
  ]
}
```

#### Workflow

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

#### Constraints

- **Anchors take zero space in their parent** — when nested inside a `stack` or `grid`, an anchor reserves no slot, so siblings collapse together. Anchors are best used at the root or as their own top-level branch.
- **Anchor `to:` must be a node already on the canvas** — local `@key` refs only work for edges, not for anchor targets in this version.
- **Never use ALL CAPS** in a text element — headings included. Use title case (e.g., "Key Tensions" not "KEY TENSIONS").
- **Text-element content** is authored as markdown — the server converts it server-side. A heading is `"# Key Tensions"` with no `colorHex`. Keep it card-sized (at most ~3 sentences; see Canvas Elements 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
- Adding a single item next to a known coordinate without invoking the solver

In all other cases — and especially when adding 3+ items at once — use `place`.

### Interlinking Notes

When creating multiple related notes, interlink at two levels:

**1. Relationship mentions inside note content** — connects notes in the knowledge graph:
- Use `cnotes notes create` with an array and `@key` placeholders for cross-references.
- Example: `[@B: The Problem](relationship:extends)` in markdown.
- The server resolves placeholders to real display IDs in one atomic transaction.

**2. Canvas edges between nodes** — visual connections on the canvas:
- After adding nodes, connect them with `cnotes canvas add-edge`, using short verb labels ("triggers", "depends on").
- Edges can join any node type, as long as both endpoints are on the same canvas.

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

#### Placeholder Syntax for `cnotes notes create`

Use `[@key: Title](relationship:type)` where `key` matches a note's `key` field in the `--notes` JSON:
```markdown
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](#content-via-markdown) above 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.

### Gathering Context with Search

Before creating or updating notes on a topic, **search existing notes first**:

1. **Notes:** `cnotes search semantic "<topic>"` — find conceptually related notes.
2. **Canvases:** `cnotes canvas search "<topic>"` — separate corpus from notes.
3. **Follow up:** Open hits with `cnotes notes get` / `cnotes canvas get`, then chase `rel list` and in-note mentions.
4. **Synthesize:** Combine what you found from 2-3 searches before acting.

### Error Handling
- Branch on the **exit code** first (see the table in [Reading output](#reading-output-start-here)) — it classes the failure without parsing prose — then read the `hint` in the `{ error, code, exitCode, hint }` JSON error on stderr; it names the command that fixes it.
- If a command fails with auth errors (exit `2`), run `cnotes auth status` to verify credentials.
- If a workspace error occurs (exit `4` not found, or `7` permission), verify with `cnotes workspace current` and list options with `cnotes workspace list`.
- If a **write** fails with exit `8` (`subscription_required`), the workspace is on the free plan. Don't dead-end the user on a billing URL: if they own it and haven't used the trial, offer to run `cnotes upgrade` (free, no card) and retry. See [Paid actions](#paid-actions-writes-need-pro).

### 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.
