Skills · fat-marker-flows

Fat-Marker Flows (Shape Up broad-strokes keyframes on a canvas)

TerminalCopy
cnotes skills install fat-marker-flows

Raw markdown: /skills/fat-marker-flows/skill.md

Turn a described flow into a small set of deliberately rough sketch images, one per key moment, and place them on a CreatorNotes canvas as a numbered left-to-right journey. The output helps a reader feel the interaction without anyone mistaking it for finished design.


The fidelity principle (do not skip)

Use fat-marker / broad-strokes fidelity (from Shape Up). Thick hand-drawn marker, rough boxes, handwritten labels, almost no detail. This is intentional:

  • It makes people react to the shape of the flow, not fonts, spacing, or color.
  • It signals "this is still wet clay, push back on it," which polished mockups suppress.
  • It is fast and cheap to regenerate when the shape changes.

Resist the urge to make it pretty. If it looks like a real screenshot, you have failed the brief.


Reference examples (look at these first)

examples/ holds a real six-frame journey produced with this skill (the CreatorNotes "Home canvas as a multi-view lens" flow). Read a couple of them before generating to calibrate the target fidelity and the coral-accent discipline:

  • 01-timeline-view.png — the starting state, with the Timeline · Manual · By theme · + tab chrome
  • 02-switch-manual-drag.png — a card mid-drag with a dashed "was-here" outline and a coral drag arrow
  • 03-flip-back-nothing-lost.png — back to Timeline, the card returned, coral check
  • 04-by-theme-zoom-out.png — clustered theme blobs
  • 05-ask-for-a-view.png — prompt bar plus a dashed Preview with Save/Discard
  • 06-save-as-lens.png — a new named tab added to the switcher

What to copy from them: the same window + tab row in every frame, one coral-highlighted change per frame, a circled number, a handwritten title, and a one-line caption stating the step's promise.


Step 0 — Scope ONE flow and list the keyframes

A keyframe is a key moment in a single flow, not a screen inventory. Before generating:

  1. Pick one flow (e.g. "drag a portal, flip back, save a view"). Don't mix flows.
  2. List 4–7 keyframes. Each is a state where something changed from the prior frame.
  3. Decide the shared chrome that stays identical across every frame (same window, same tab bar, same panel). Only the changed thing differs frame to frame. Consistent chrome is what makes the set read as one continuous journey.
  4. For each frame, write: a circled number, a short title, what the user just did, the one element to highlight, and a one-line caption that states the promise of that step.

Keep captions free of em dashes (house style).


Step 1 — The reusable style preamble

Prepend this verbatim to every frame's prompt so the set is visually consistent:

Fat-marker product sketch in the Shape Up "broad strokes" style: thick black marker
on off-white paper, deliberately rough and low-fidelity, hand-drawn rectangles and
handwritten labels, like a quick whiteboard wireframe. NOT a polished UI: no gradients,
no realistic chrome, no photorealism, no fine detail. Use ONE accent color, a warm
coral-pink, and ONLY on the single active or changed element. Lots of whitespace. It
should read as one storyboard keyframe that shows the SHAPE of an interaction, not its
visual design.

(The coral accent matches the CreatorNotes brand. Swap the accent if the project's brand differs, but keep it to ONE color used only on the changed element.)

Then add the frame-specific content: the circled number, the title, the shared chrome (describe it the same way every frame), and the one thing that changed. Be concrete and literal so the model draws the right boxes and labels.


Before you generate (image access)

Keyframes need an image model, and there are two modes — work out which applies BEFORE frame 1, so you don't generate six frames only to discover the path was blocked:

  • You're an admin (your email is on the server allowlist) → cnotes files gen-image just works server-side, no key. This is the default.
  • Everyone else (most people) → the default server path returns an admin_only error. Add --byo-key and set GOOGLE_GENERATIVE_AI_API_KEY first (a free key from https://ai.google.dev).

If you're not sure which applies, tell the user up front rather than guessing: "Generating the sketches needs a free Google AI Studio key — grab one at ai.google.dev and I'll set it as GOOGLE_GENERATIVE_AI_API_KEY." Then pass --byo-key on every gen-image call below.

The key stays on this machine. With --byo-key, the image is generated by a direct call from your own machine to Google's API; only the finished PNG is uploaded to CreatorNotes. The key is read from your local environment and is never sent to CreatorNotes servers — so you can honestly reassure the user it is client-side only.


Step 2 — Generate (test one, then batch)

Generate each frame with cnotes files gen-image: it produces the image with an image model and stores it in the workspace, returning a ready-to-use URL. No local API key and no repo checkout needed. The prompt goes on stdin (so you don't escape quotes); use --aspect 16:9 for screen-like frames and --json so you can read .storageId and .fullUrl.

WS=<workspaceId>     # e.g. your Private workspace for internal work
mkdir -p /tmp/mockups && cd /tmp/mockups

# Frame 1 — generate, capture the JSON, then LOOK at it before batching the rest.
cnotes files gen-image --aspect 16:9 -w "$WS" --json <<'PROMPT' > kf1.json
<style preamble>

Content of this frame:
- A hand-drawn circled number "1" in the top-left corner.
- Handwritten title across the top: "...".
- <shared chrome described literally>
- <the one highlighted/changed element, in coral>
- A handwritten caption along the bottom: "...".
PROMPT

# Download the pixels and Read them to confirm the style landed.
cnotes files get "$(node -e 'console.log(JSON.parse(require("fs").readFileSync("kf1.json","utf8")).storageId)')" -o /tmp/mockups/kf1.png

Always generate frame 1 first and look at it (Read /tmp/mockups/kf1.png) to confirm the fidelity lands before spending calls on the rest. Once it's right, generate the remaining frames the same way (one gen-image call each), re-pasting the same style preamble and the same chrome description in every frame. Collect each frame's .fullUrl, in frame order, into urls.txt:

# Repeat per frame; appends the fullUrl in order.
cnotes files gen-image --aspect 16:9 -w "$WS" --json <<'PROMPT' \
  | node -e 'let s="";process.stdin.on("data",d=>s+=d).on("end",()=>console.log(JSON.parse(s).fullUrl))' >> urls.txt
<style preamble + this frame's content>
PROMPT

Then look at every frame and check: same chrome across all, coral only on the changed thing, numbers in order, captions legible, the sequence tells the story.

Admins generate server-side with no local key. Everyone else adds --byo-key after setting GOOGLE_GENERATIVE_AI_API_KEY (get a key at ai.google.dev): the image is then generated on the local machine and uploaded, and the key never leaves it. See cnotes files gen-image --help.


Step 3 — Place on the canvas as a journey filmstrip

Use the cnotes CLI. Wrap the canvas writes in an operation. Step 2 already stored each image and gave you a fullUrl per frame (in urls.txt, frame order), so there is no separate upload step — just place the URLs as a horizontal stack (the filmstrip) with a header above.

WS=<workspaceId>
CANVAS=<canvasId>
cd /tmp/mockups   # where urls.txt from Step 2 lives

cnotes operations begin -w "$WS" --prompt "Add fat-marker user-journey keyframes for <flow>" --json >/dev/null
# urls.txt already holds one fullUrl per line, in frame order, from Step 2.

Build a place spec where each frame is a richtext item whose content is just ![Keyframe N](<fullUrl>), wrapped in a horizontal stack, with a header richtext above it:

// build-journey.js — writes journey.json
const fs = require('fs');
const urls = fs.readFileSync('urls.txt','utf8').trim().split('\n');
const imgs = urls.map((u,i)=>({kind:'item',type:'richtext',size:'medium',key:`kf${i+1}`,content:`![Keyframe ${i+1}](${u})`}));
const spec = { root: { kind:'stack', axis:'vertical', gap:'medium', align:'start', items:[
  {kind:'item',type:'richtext',size:'medium',content:'# User journey: <flow>\n\nFat-marker keyframes (broad strokes). Read left to right, 1 to N. Coral marks the active or changed thing.'},
  {kind:'stack', axis:'horizontal', gap:'medium', align:'start', items:imgs},
]}};
fs.writeFileSync('journey.json', JSON.stringify(spec));
node build-journey.js
cnotes canvas place $CANVAS -w $WS --spec ./journey.json --json > placed.json

Step 4 — Position the band (the bulk-move offset trick)

cnotes canvas place ignores an absolute origin and appends the new block below existing content. To put the filmstrip somewhere specific (e.g. a hero band above the title), read the returned positions, compute one uniform offset, and bulk-move the whole group so the relative layout is preserved:

// move-to-top.js
const fs=require('fs'); const p=require('./placed.json'); const items=p.items||[];
const minY=Math.min(...items.map(i=>i.positionY));
const dy=-1500-minY;  // move the group's top edge to y = -1500 (above content at y=100)
const moves=items.map(i=>({nodeId:i.id,nodeType:'richtext',x:i.positionX,y:Math.round(i.positionY+dy)}));
fs.writeFileSync('moves.json',JSON.stringify(moves));
node move-to-top.js
cnotes canvas bulk-move $CANVAS -w $WS --moves "$(cat moves.json)"
cnotes operations end -w $WS

Gotchas (learned the hard way)

  • Carry the sequence with circled numbers and left-to-right layout, not drawn arrows. The numbered filmstrip reads as a journey at a glance; arrows between frames add clutter. (Edges between richtext/image stickies do now connect if you need one — e.g. a labeled "then" between two moments — but reach for them sparingly; the layout is the primary device.)
  • place ignores absolute origin and appends below existing content. Use the bulk-move offset trick above to reposition.
  • richtext size presets: large ≈ 1680px wide, medium ≈ 1120px, small ≈ 560px. For a 6-frame filmstrip, medium is the sweet spot; large makes a ~11,000px strip nobody can pan.
  • Don't verify by grepping ![ in node content. CreatorNotes converts the image markdown into a TipTap image node on save, so the raw content no longer contains ![. Verify instead by counting richtext nodes / their positions (e.g. nodes with negative Y for a top band).
  • No local key needed (admin). cnotes files gen-image generates server-side, so there's nothing to export. Non-admins pass --byo-key after setting GOOGLE_GENERATIVE_AI_API_KEY; the image is generated locally and the key never leaves the machine.
  • Internal/working mockups go in the Private workspace. Use -w <privateId> and verify with cnotes workspace current.
  • One change per frame. If two things differ between adjacent frames, split into two frames. Ambiguity about "what changed" is what makes a storyboard unreadable.

When NOT to use this

  • The user wants production-ready, pixel-accurate UI: use a real design tool, not fat-marker sketches.
  • It's a single static screen with no flow: one sketch is fine, skip the journey scaffolding.
  • The decision is still about the problem, not a solution shape: shape it in words first (see the shaping / framing-doc skills) before drawing keyframes of a solution you haven't chosen.