
Generate Project Plan
- 16 installs
- 1.9k repo stars
- Updated August 4, 2026
- figma/mcp-server-guide
generate-project-plan builds confirmed FigJam project plan boards from PRDs.
About
The generate-project-plan skill runs a multi-step FigJam workflow from PRD input and optional codebase grounding. It loads figma-use once per session, re-loads figma-use-figjam before each use_figma call, and figma-generate-diagram before generate_diagram. Foundation references cover palette, layout, plugin API traps, and codebase grounding rules. A section catalog proposes candidate sections the user selects before per-section deep research and block-shape confirmation for tables, stickies, multi-column text, diagrams, and metadata strips via dedicated block subskills. Steps progress research, section proposal, content confirmation, skeleton creation, fill passes, diagrams, and wrap. Triggered by project plan in FigJam requests or /generate-project-plan with PRD URLs.
- Proposes section catalog from PRD and codebase research.
- Confirms content and block shape per section interactively.
- Loads FigJam foundation and block subskills before API calls.
- Creates skeleton then fills tables, stickies, and diagrams.
- Grounds plans in PRD plus optional repository context.
Generate Project Plan by the numbers
- 16 all-time installs (skills.sh)
- Ranked #2,099 of 3,282 Productivity & Planning skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
generate-project-plan capabilities & compatibility
- Capabilities
- mandatory foundation skills list · per block subskill reference table · interactive per section confirmation flow
- Works with
- figma
- Use cases
- planning · project management
What generate-project-plan says it does
Turn a PRD (plus optional codebase grounding) into a FigJam project plan board
npx skills add https://github.com/figma/mcp-server-guide --skill generate-project-planAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 16 |
|---|---|
| repo stars | ★ 1.9k |
| Last updated | August 4, 2026 |
| Repository | figma/mcp-server-guide ↗ |
How do I generate a FigJam project plan from a PRD?
Turn a PRD plus codebase context into an interactive FigJam project plan board.
Who is it for?
PMs and tech leads visualizing delivery plans in FigJam.
Skip if: Skip for static slide decks without FigJam MCP.
When should I use this skill?
User wants FigJam project plan, /generate-project-plan, or PRD board.
What you get
FigJam board with user-approved sections, blocks, and diagrams.
Files
generate-project-plan
Turn a PRD (plus optional codebase grounding) into a FigJam project plan board. Section set is not fixed — the skill proposes candidates from the research and the user picks which to include. For each picked section, the skill proposes content + rendering shape (block) and the user confirms.
Mandatory prerequisites
Foundation skills (load by name; available in figma/mcp-server-guide):
figma-use— Load once per session. Stays in context for alluse_figmacalls.figma-use-figjam— Re-load before every `use_figma` call.figma-generate-diagram— Re-load before every `generate_diagram` call.
Foundation references (in this plugin):
- `foundation/palette.md` — section + sticky + text palette constants (
hex/255). - `foundation/layout.md` — canvas geometry, sizing rules, placeholder lifecycle.
- `foundation/plugin-api-traps.md` — documented traps for FigJam
use_figma. - `foundation/codebase-grounding.md` — Step 1 expansion rules.
Section catalog:
- `section-catalog.md` — the ~10 candidate sections with default blocks and palette.
Block subskills (one per content type — re-load the one(s) you need before each use_figma fill call):
| Block | File | When to load |
|---|---|---|
| Top-level section | `blocks/section.md` | Skeleton pass (Step 6) and every fill call (Step 7) |
| Nested section | `blocks/nested-section.md` | Fills that group sub-content (e.g. "Design Decisions 1/2/3") |
| Intro callout | `blocks/intro-callout.md` | Fills that open with a highlighted intro (e.g. Motivation) |
| Text primitives | `blocks/text-primitives.md` | Any fill that uses body paragraphs, H3 subheaders, or bulleted lists |
| Table | `blocks/table.md` | Fills with structured data (Resources, Goals, Dependencies, Rollout, Milestones) |
| Multi-column text | `blocks/multi-column-text.md` | Fills with 2–4 option columns (Design Decisions alternatives) |
| Sticky column | `blocks/sticky-column.md` | Fills with lists of stickies (Success Metrics, Risks, Open Questions) |
| Diagram section | `blocks/diagram-section.md` | Right-column diagram sections (Step 8) |
| Metadata strip | `blocks/metadata-strip.md` | Skeleton pass — one metadata strip at top of board |
Also pass skillNames: "figma-use,figma-use-figjam,generate-project-plan" on use_figma calls (logging only).
Visual UI conventions — STRICT, do not deviate
These are derived from a canonical reference board. Read the source-of-truth files for the full constants; this section is a single-place summary so an agent can answer "what color / size / font / padding?" without hunting.
Colors (two-tone per section)
Every left-column section uses two coordinated colors of the same hue: a very-pale ARCH_PALE background, and a slightly-more-saturated FigJam-SECTION palette color for any table header inside that section. Right-column diagram sections are pure white.
Section bg (ARCH_PALE.X) | Table header (TABLE_HEADER.lightX) | Hue |
|---|---|---|
#F8F5FF | #DCCCFF | violet |
#EBFFEE | #CDF4D3 | green |
#DBF0FF | #C2E5FF | blue |
#F5FBFF (alt) | #C2E5FF | pale blue |
#FFF7F0 | #FFE0C2 | orange |
#F1FEFD | #C6FAF6 | teal |
#FFFBF0 | #FFEC BD | yellow |
#FFEEF8 | #FFC2EC | pink |
#FFEEE8 | #FFCDC2 | red |
Source: references/foundation/palette.md. Never use the dark-saturated palette (#874FFF, #3DADFF, etc.) for table headers — that's for FigJam's standalone tables, not project-plan boards.
Architecture-diagram subgraph colors are auto-applied by generate_diagram and must not be overridden. Their canonical values: client #AFF4C6 rounded-rect, gateway #FFFFFF square (diamond if labeled "Load Balancer"/"ALB"/"LB"), service #E4CCFF square, datastore #BDE3FF cylinder, external #FFFFFF PREDEFINED_PROCESS, async #BDE3FF ENG_QUEUE.
Typography (font sizes)
| Element | Size | Font | Color |
|---|---|---|---|
| H1 (board title) | 40 | Inter Medium | #1E1E1E |
| H2 (section title — first child of every section) | 40 | Inter Medium | #1E1E1E |
| H3 — full-width subhead (e.g. "Resources" inside Motivation) | 40 | Inter Medium | #1E1E1E |
| H3 — nested-section header (e.g. "Design Decision 1: …") | 32 | Inter Medium | #1E1E1E |
| H3 — column title in 2/3/4-col layouts (Risks col, Goals col) | 24 | Inter Medium | #1E1E1E |
| Body text | 16 | Inter Medium | #1E1E1E |
| Table cells (header AND body) | 16 | Inter Bold | #1E1E1E |
The three different H3 sizes are deliberate. 40 = matches H2 weight when subhead is alone in the section. 32 = sub-section header inside a child section (672px inner width). 24 = column title in narrow contexts (≤ 224px col width). Pick by container width, not by semantic depth.
Always load both Inter Medium AND Inter Bold at the top of any use_figma script that creates tables (Bold) plus any other text (Medium).
Section properties
| Property | Value |
|---|---|
section.fills | [{ type: 'SOLID', color: ARCH_PALE.X }] (left column) or ARCH_PALE.white (right column / diagrams) |
section.name | "" — empty string. NO FigJam title-bar label. The H2 inside is the only title. |
| Inner padding (all 4 sides) | 32 (current default; reference uses 40-50, kept at 32 for now) |
| First child position | (32, 32) |
| Width (left column) | 800 |
| Width (right column / diagram) | max(1200, diag.width + 64) after diagram is reparented |
| Vertical gap between sections (inside the wrapper) | 64 |
| Hug behavior | Manual: call section.resizeWithoutConstraints(w, maxChildBottom + 32) after appending children. Sections do NOT auto-grow. |
| Placeholder during build | placeholder = true in skeleton pass; placeholder = false at the end of the section's fill. |
Outer wrapper + column alignment (STRICT defaults)
The board has one outer wrapper (unlabeled, white) plus the diagram column:
1. Column wrapper — an unlabeled white SECTION at (_, 0). Contains, in order from top:
- H1 project title (40px Inter Medium, charcoal) at
(64, 64)(= section padding) - Body row of metadata: Owner / Status / Last updated / Source (16px Inter Medium, charcoal), at
(64, h1.y + h1.height + 16)— 16px gap below H1; 32px gap between each body cell - 64px gap below the body row, then the 6 left-column sections stacked with 64px gutter
2. Diagram column at (columnWrapperRight + 64, 0). Each diagram is its own un-wrapped white SECTION; the top diagram's y aligns with the column wrapper's y (= 0). Diagrams stack with 64px gutter.
The metadata is embedded in the column wrapper (NOT a separate section). One column wrapper holds everything text-related.
Constants:
- Wrapper inner padding (all 4 sides): 64
- Vertical gap between sections inside the wrapper: 64
- Horizontal gap between wrapper right edge and diagram-column left edge: 64
- Top of the diagram column aligns with
wrapper.y(same horizontal axis as the wrapper top edge) - Vertical gap between stacked diagram sections: 64 (matches the inner gutter)
const PAD = 64;
const leftIds = [/* all left-column section ids in order */];
const sections = [];
for (const id of leftIds) sections.push(await figma.getNodeByIdAsync(id));
// Compute bbox of the column in page coords
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
for (const s of sections) {
minX = Math.min(minX, s.x); minY = Math.min(minY, s.y);
maxX = Math.max(maxX, s.x + s.width); maxY = Math.max(maxY, s.y + s.height);
}
const wrapper = figma.createSection();
wrapper.name = ""; // STRICT
wrapper.fills = [{ type: 'SOLID', color: WHITE }]; // ARCH_PALE.white
wrapper.resizeWithoutConstraints((maxX - minX) + 2*PAD, (maxY - minY) + 2*PAD);
wrapper.x = minX - PAD;
wrapper.y = minY - PAD;
// Reparent + translate to keep visual positions
for (const s of sections) {
const newX = (s.x - minX) + PAD;
const newY = (s.y - minY) + PAD;
wrapper.appendChild(s);
s.x = newX;
s.y = newY;
}The wrapper has no header (no H2 inside) and no name — it's a pure container. Don't repeat the project title here; that lives in the metadata strip.
Position the diagram column after the wrapper exists:
const wrapperRight = wrapper.x + wrapper.width;
const diagramX = wrapperRight + 64; // 64px horizontal gap matches inter-section gutter
let y = wrapper.y; // align top with wrapper's top edge
for (const id of diagramSectionIdsInOrder) {
const d = await figma.getNodeByIdAsync(id);
d.x = diagramX;
d.y = y;
y += d.height + 64; // 64px vertical gutter between stacked diagrams
}Vertical spacing (STRICT)
| Between | Gap |
|---|---|
| Section top edge → H2 (first child) | 32 (= padding) |
| H2 → next child (body / intro callout / H3 / table / first column) | 24 |
| Body paragraph → H3 | 24 |
| H3 (any size: 40 / 32 / 24) → next child (body / table / list / column content) | 24 |
| Body → body | 24 |
| List → next block | 24 |
| Last child bottom → section bottom edge | 32 (= padding) |
Always position children using `prevChild.y + prevChild.height + 24` — never use a fixed offset like prevChild.y + 60. The H3 has three different sizes (40 / 32 / 24), so a fixed offset is wrong by definition; always read prevChild.height after the font size is set.
When you change a header's font size after the fact, you MUST re-stack downstream children. Setting h3.fontSize = 40 grows the node's height; if the next child's y was computed before the resize, it will overlap.
Tables
- Both header AND body cells use
Inter Bold16px (NOT Medium). - Header text is
#1E1E1Echarcoal on a light fill (matching the section's hue). - Body cells leave fill at default white; text is charcoal.
- Headers do NOT use white-on-dark — that's wrong for this board style.
Diagrams (right column)
generate_diagram with useArchitectureLayoutCode: "FIGMA_DIAGRAM_2026" produces multiple page-level nodes (1–2 subgraph SECTIONs + bare SHAPE_WITH_TEXTs + CONNECTORs), NOT a single container. To wrap them in a section: 1. Collect all new page-level nodes (exclude known plan-section IDs). 2. Compute the bbox. 3. Create a new SECTION sized to bboxW + 64 × bboxH + 64 + 64 (HEADER_BLOCK = 40 H2 + 24 gap), fill ARCH_PALE.white. 4. Reparent each diagram node, translating local coords to maintain visual layout. 5. CRITICAL — delete and recreate every connector after reparent. The assign-to-self trick (c.connectorStart = c.connectorStart) is NOT reliable: short connectors re-route fine, but long-bend connectors retain stale elbow waypoints and extend hundreds of pixels outside the section. Delete + figma.createConnector() from captured spec produces a clean route every time. See `blocks/diagram-section.md` for the spec-capture + recreate pattern. 6. Connector labels — explicit `fontName` + `fontSize` + `fills` ALL required. A fresh connector's text sublayer has no usable defaults. Set ALL FOUR: c.text.fontName = { family: 'Inter', style: 'Medium' }, c.text.fontSize = 14, c.text.characters = label, c.text.fills = [{ type: 'SOLID', color: CHARCOAL }]. Default text.fills is `[]` (empty array) so the label renders transparent and is invisible — read-back of c.text.characters lies; verify with a screenshot.
Diagram-section convention: section.name = "", H2 text node inside as the title (matches the rest of the board).
What NOT to use (common wrong defaults)
| Don't use | Use instead | Why |
|---|---|---|
#CDF4D3 (FigJam lightGreen) for section bg | #EBFFEE (ARCH_PALE.green) | Too saturated next to diagrams |
#C2E5FF (FigJam lightBlue) for section bg | #DBF0FF or #F5FBFF (ARCH_PALE.blue / blueLite) | Same |
#874FFF (FigJam dark violet) for table header | #DCCCFF (FigJam lightViolet) | Reference uses light-on-pale, not dark-on-pale |
| White text on dark table header | #1E1E1E charcoal text on light header | Same — pale-on-pale convention |
Inter Medium for table cells | Inter Bold | Reference uses Bold for all cells |
section.name = "Goals" (or any non-empty string) | section.name = "" | Reference uses empty names; H2 inside is the title |
| Reparenting only one subgraph from a generated diagram | Reparent ALL page-level nodes (SECTIONs + SHAPEs + CONNECTORs) | Architecture diagrams are not single nodes |
Trusting c.connectorStart = c.connectorStart to re-route every connector | After reparent, delete and recreate every connector from a captured spec | Long-bend connectors retain stale elbow waypoints; only figma.createConnector() produces a fresh route |
Setting only c.text.characters = label on a fresh connector | Set c.text.fontName, c.text.fontSize, c.text.characters, AND c.text.fills | Defaults: fontSize=missing, fills=[] (empty array → transparent) → label invisible despite correct read-back |
Step template
Every step below uses this shape. Read the step, then execute.
## Step N — <Name> [Type: Research | Confirm | Write]
Inputs required: …
Ask if missing: …
Tools / refs loaded: …
Do: (3–6 action bullets)
Checkpoint: (Research → self-check; Confirm → AskUserQuestion; Write → screenshot + AskUserQuestion)Three step types:
- Research — read-only; checkpoint = self-check list.
- Confirm — no board writes, user decision gate; checkpoint =
AskUserQuestion. - Write — creates/mutates FigJam; checkpoint = screenshot +
AskUserQuestion.
---
Step 1 — Gather context [Research]
Inputs required
- PRD file path or pasted text.
- Optional: codebase entry points (file paths, service names, doc paths).
Ask if missing
- "Where's the PRD? (path or paste)."
- "Any codebase entry points I should ground in? (paths / services / docs / 'none')."
Tools / refs loaded
Read,Glob,Grep.- `foundation/codebase-grounding.md`.
Do 1. Read the PRD. Extract: title, problem, goals, non-goals, owner, audience, success metrics, rollout hints, risks. 2. If entry points provided: follow codebase-grounding.md — bounded 20-file cap, depth-1 imports, walk up to CLAUDE.md/ARCHITECTURE.md/OWNERS. 3. Produce the tech-context object: files_read, services, external_deps, key_modules, architecture_notes, ownership, expansion_truncated.
Self-check
- Have: project title, problem statement, at least 1 concrete goal, owner (or "TBD"), services touched (or empty list with a reason).
- Enough signal to draft candidate section cards in Step 2. If not, loop back and ask.
---
Step 2 — Propose candidate sections [Confirm]
Inputs required
- Tech-context object from Step 1.
Tools / refs loaded
- `section-catalog.md`.
AskUserQuestion.
Do 1. For each section in the catalog, decide if there is real content for it from Step 1. Skip catalog entries that would be empty or padding. 2. For each qualifying candidate, produce a card:
- Title (catalog name)
- 1-line description (what this section will contain, specific to the PRD)
- Why suggested (which PRD facts or tech-context items justify it)
- Default block shape (from the catalog)
3. Print all cards to chat. 4. Fire AskUserQuestion with a multiSelect question per batch of ≤4 candidates (max 4 questions per call, 4 options each → up to 16 candidates per call). Each option's label is the section title; description is the 1-line summary.
Checkpoint (AskUserQuestion)
- The multiSelect questions above. User ticks the sections they want. Store the selected set as
approved_sections. - If zero sections selected → stop with a clean exit message. No file is created.
---
Step 3 — Per-section deep research [Research]
Inputs required
approved_sectionsfrom Step 2; tech-context from Step 1.
Tools / refs loaded
Read,Grep,Glob(optional).
Do 1. For each section in approved_sections, look up its catalog entry. The catalog declares what the section needs (e.g. Dependencies needs cross-team services + external deps + blockers). 2. Compare what the section needs vs. what the tech-context has. 3. Produce a gap list per section: specific facts the user must supply, framed as answerable questions (no "figure it out yourself" gaps).
Self-check
- Every
approved_sectionsentry has eitherready(no gaps) or a specific non-empty gap list. - Gaps are answerable — not vague prompts like "tell me more about X."
---
Step 4 — Per-section content + block proposal [Confirm]
Inputs required
- Gap lists from Step 3.
Tools / refs loaded
AskUserQuestion.- `section-catalog.md`.
- Block reference(s) for the section's default shape (e.g.
blocks/table.mdfor Dependencies).
Do, per section (one at a time, or small batch if trivial): 1. Fill the gap list — free-text prompt for prose; AskUserQuestion for bounded choices. 2. Propose:
- Content: the concrete bullets / rows / stickies that will appear.
- Block shape: the rendering block (body paragraph / table / multi-column / sticky column / …). Default from
section-catalog.md; offer alternative shapes where sensible (e.g. "As a table, or as a multi-column layout?").
3. Show a short preview — section title + first line of body + block type summary. 4. Fire AskUserQuestion: "Use this content + shape? [Yes / Edit / Skip this section]." 5. Edit → accept free-text amendments, re-show, re-ask. Skip → mark the section as skipped; do not write it to the board.
Checkpoint
- Every section is
approved,edited+approved, orskipped. No board writes yet.
---
Step 5 — Create FigJam file [Write]
Inputs required
approved_sections(non-skipped); project title;planKey(Figma team plan).
Tools / refs loaded
create_new_fileMCP tool.whoamiMCP tool (forplanKeyif not known).use_figma(once).figma-use(already in context from Step 5 onward).figma-use-figjam(re-loaded for the probe).AskUserQuestion.
Do 1. Resolve planKey: call whoami. If one plan → use it. If multiple → AskUserQuestion which team. 2. Call create_new_file with { planKey, fileName: "<project title>", editorType: "figjam" }. Capture file_key + file_url. 3. Run the first-run probe (use_figma):
const page = figma.currentPage;
return {
rootName: figma.root.name,
editorType: figma.editorType,
pageCount: figma.root.children.length,
firstPageName: figma.root.children[0].name,
currentPageChildrenCount: page.children.length,
};Expect editorType === "figjam" and empty page. If not, halt and report.
Checkpoint (probe output + AskUserQuestion)
- Print probe return +
file_url. - AskUserQuestion: "File created at
<file_url>— proceed to skeleton? [Yes / Cancel]." Cancel = stop, leave empty file.
---
Step 6 — Skeleton pass [Write]
Inputs required
approved_sectionsin taxonomy order; palette fromfoundation/palette.md; layout constants fromfoundation/layout.md.
Tools / refs loaded
use_figma(one call).- Re-load
figma-use-figjam/SKILL.md. - Re-load
blocks/section.mdandblocks/metadata-strip.md. - `foundation/palette.md`, `foundation/layout.md`.
Do 1. Create metadata strip per blocks/metadata-strip.md (H1 + 4 body texts at board (0, 0)–(0, ~100)). 2. For each approved section, create a top-level SECTION per blocks/section.md — colored bg from ARCH_PALE, section.name = "" (no title-bar label — STRICT), placeholder = true, resizeWithoutConstraints(LEFT_COL_W, DEFAULT_H) for left-column sections or resizeWithoutConstraints(RIGHT_COL_W_MIN, DEFAULT_H) for right-column diagram sections. 3. Position each left-column section at (0, SECTION_TOP_Y + cumulative_y). Right-column sections at (832, SECTION_TOP_Y + cumulative_y_right). 4. Return all created node IDs: { createdNodeIds: { metadataStrip: {...}, sections: { <slug>: "<id>", ... } }, status: "skeleton-complete" }. (The slug is internal, used to look up palette + drive Step 7 fills. It is never written to section.name.) 5. Take an inline screenshot at await figma.currentPage.screenshot({ scale: 0.3 }).
Checkpoint (screenshot + AskUserQuestion)
- AskUserQuestion: "Skeleton looks right? [Yes / Fix / Cancel]." Fix = targeted fix script; Cancel = stop.
---
Step 7 — Fill pass (one call per section) [Write]
Inputs required
- Approved content + block shape for each section; section ID from Step 6.
Tools / refs loaded
use_figma(one call per section).- Re-load
figma-use-figjam/SKILL.md. - Re-load whichever block refs this section uses:
blocks/section.mdalways.blocks/text-primitives.mdif body/H3/list.blocks/table.mdif table.blocks/multi-column-text.mdif multi-column.blocks/nested-section.mdif nested subsections.blocks/intro-callout.mdif highlighted intro.blocks/sticky-column.mdif stickies.
Do, per section: 1. await figma.getNodeByIdAsync(sectionId) — confirm type is SECTION. 2. Build content: H2 header first, then children per approved block shape. Append to section FIRST, then set x/y. 3. Two-pass measure for stickies per blocks/sticky-column.md. 4. section.resizeWithoutConstraints(LEFT_COL_W, computed_height). 5. section.placeholder = false. 6. await section.screenshot() inline. 7. return { mutatedNodeIds: [...], sectionHeight, screenshotIncluded: true }.
Checkpoint (screenshot + AskUserQuestion)
- Per section: AskUserQuestion: "Section
<name>done? [Yes / Edit this section / Skip rest]." Edit = targeted fix; Skip rest = exit fill loop (user will finalize manually).
After all left-column fills: run the re-stack pass (single use_figma) to fix cumulative Y based on actual resized heights:
let y = SECTION_TOP_Y;
for (const id of leftColumnSectionIdsInOrder) {
const sec = await figma.getNodeByIdAsync(id);
sec.y = y;
sec.x = 0;
y += sec.height + 32;
}
return { mutatedNodeIds: leftColumnSectionIdsInOrder };Then run the outer-wrapper pass (single use_figma) — wrap all left-column sections in an unlabeled white outer SECTION (see "Outer column wrapper" in the visual conventions block above). This is STRICT; do not skip.
---
Step 8 — Diagrams [Write]
Inputs required
- Diagram intents from Step 2/4 (Current State, Target State, 0–N Key Flows); tech-context for Mermaid composition.
Tools / refs loaded
- Re-load
generate-diagram/SKILL.mdbefore eachgenerate_diagramcall. - Re-load
figma-use-figjam/SKILL.md+blocks/diagram-section.mdbefore each reparentuse_figmacall. generate_diagramMCP tool,use_figma.
Do, per diagram: 1. use_figma: create the right-column section (white fill, H2 header, placeholder = true) per blocks/diagram-section.md. 2. generate_diagram: compose Mermaid from tech-context; architecture diagrams use useArchitectureLayoutCode: "FIGMA_DIAGRAM_2026". 3. use_figma: locate the generated diagram node, reparent into the section (section.appendChild(diag)), position below H2, resize section to fit. placeholder = false. 4. await section.screenshot().
Failure handling: if generate_diagram fails, leave a text placeholder in the section reading "Diagram generation failed: <message>. Regenerate manually." Continue to the next diagram.
Checkpoint (screenshot + AskUserQuestion)
- Per diagram: AskUserQuestion: "Diagram
<name>looks right? [Yes / Regenerate / Skip]."
---
Step 9 — Final review + report [Write, then Read]
Inputs required
file_urlfrom Step 5.
Tools / refs loaded
use_figma(one call for full-page screenshot).
Do 1. await figma.currentPage.screenshot({ scale: 0.25 }) — full board. 2. Inspect: two-column structure, no overlaps, no sections left with placeholder = true, metadata strip visible. 3. Fix any issues with a targeted use_figma script. Do not regenerate from scratch. 4. Post to chat:
✅ Project plan written to FigJam.
File: <file_url>
Sections: <N text + N diagram>
Files referenced during grounding: <count>Checkpoint (screenshot + AskUserQuestion)
- AskUserQuestion: "Done, or tweak? [Done / Tweak]." Tweak = targeted fix script on user's request, not regeneration.
---
Operational rules
- ≤10 logical operations per
use_figmacall. - Always return
createdNodeIds/mutatedNodeIdsfrom every write script. - Use
hex/255notation for all palette colors (seefoundation/palette.md). - STRICT: section backgrounds use the `ARCH_PALE` palette, NOT the FigJam standard SECTION palette. ARCH_PALE colors (
#EBFFEE,#F8F5FF,#F5FBFF,#FFF7F0, etc.) visually pair with the architecture-diagram subgraph wrappers thatgenerate_diagramproduces. The FigJam SECTION palette (#CDF4D3,#C2E5FF,#DCCCFF,#FFE0C2) is too saturated and causes visible color clash next to diagrams. Seefoundation/palette.md. - STRICT: `section.name = ""` on every project-plan section (left-column, right-column, and nested children). The user-facing title is rendered as the H2 text node inside the section, NOT via FigJam's section title-bar label. The reference board uses empty section names; setting a non-empty
nameproduces a duplicate label that visually clutters the board. - Read, edit, or cancel at every Confirm/Write checkpoint — never write past an unanswered AskUserQuestion.
- If a
use_figmascript errors: atomic — no changes made. Read the error, fix, retry.
Trigger phrases
"/generate-project-plan", "interactive project plan", "project plan", "make a FigJam project plan", "PRD to FigJam".
Block: Diagram section
A right-column SECTION that wraps a generate_diagram result. Architecture diagrams always go here; never on the left.
When to use
- Current State Architecture
- Target State Architecture
- Key Flow diagrams (0–N)
Properties
- Section fill:
ARCH_PALE.white(right-column diagram sections are white). - Section width:
max(RIGHT_COL_W_MIN, diag.width + 64)after diagram is reparented. - Section height:
h2.y + h2.height + 16 + diag.height + 32. - Section padding: 32 on all sides, same as left column.
- Placeholder:
trueuntil the diagram is successfully reparented. - STRICT — `section.name = ""` (no FigJam title-bar label). The H2 text node inside is the only label.
- H2 header: section title (e.g.
"Current State Architecture").
Three-call sequence
Call 1 — Create skeleton section
use_figma:
const section = figma.createSection();
section.name = ""; // STRICT — no section label
section.fills = [{ type: 'SOLID', color: SECTION.white }];
section.resizeWithoutConstraints(RIGHT_COL_W_MIN, 400);
section.x = 832; // RIGHT_COL_X
section.y = SECTION_TOP_Y + cumulative_y_right;
section.placeholder = true;
const font = { family: 'Inter', style: 'Medium' };
await figma.loadFontAsync(font);
const h2 = figma.createText();
h2.fontName = font;
h2.fontSize = 40;
h2.characters = "Current State Architecture";
h2.fills = [{ type: 'SOLID', color: CHARCOAL }];
section.appendChild(h2);
h2.x = 32;
h2.y = 32;
return { createdNodeIds: { section: section.id, h2: h2.id } };Call 2 — generate_diagram
Use the generate_diagram MCP tool. Compose Mermaid from tech-context (see generate-diagram skill — re-load its SKILL.md first). Architecture diagrams:
{
mermaid: "<mermaid source>",
useArchitectureLayoutCode: "FIGMA_DIAGRAM_2026",
editorType: "figjam"
}Capture whatever the tool returns — diagram node ID, or nothing (then fall back to scanning page children).
Call 3 — Reparent + resize
CRITICAL: architecture diagrams are NOT a single node. generate_diagram with FIGMA_DIAGRAM_2026 produces:
- 1–2 subgraph
SECTIONnodes (only the categories Figma's layout promotes to sections — observed:ClientsandCore Servicesbecome sections; others get inlined) - Multiple
SHAPE_WITH_TEXTnodes at the page level (one per remaining subgraph node) - Multiple
CONNECTORnodes at the page level
Reparenting only one of these (e.g. the Core Services subgraph) breaks the diagram. You MUST wrap all new page-level nodes.
Flow:
1. Collect all new page-level nodes. Exclude your known plan nodes (metadata strip + previously-created sections). Everything else is part of the diagram. 2. Compute the bounding box across all diagram nodes. 3. Create a new section (or resize the existing placeholder) sized to bboxW + 64 × bboxH + 56 + 64 (64 = padding, 56 = H2 + gap). 4. Reparent each diagram node: section.appendChild(n); n.x = (originalPageX - minX) + 32; n.y = (originalPageY - minY) + 32 + 56. 5. Force connectors to re-route after moving their endpoints (see trap below).
const h = (r, g, b) => ({ r: r/255, g: g/255, b: b/255 });
const CHARCOAL = h(0x1E, 0x1E, 0x1E);
const WHITE = h(0xFF, 0xFF, 0xFF);
// Exclude your plan's known node IDs (sections + metadata strip)
const planIds = new Set([/* ...your plan IDs... */]);
const diagramNodes = figma.currentPage.children.filter(n => !planIds.has(n.id));
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
for (const n of diagramNodes) {
minX = Math.min(minX, n.x);
minY = Math.min(minY, n.y);
maxX = Math.max(maxX, n.x + n.width);
maxY = Math.max(maxY, n.y + n.height);
}
const bboxW = maxX - minX;
const bboxH = maxY - minY;
const SECTION_PAD = 32;
const HEADER_BLOCK = 56; // 40 H2 + 16 gap
const section = figma.createSection();
section.name = ""; // STRICT — no section label
section.fills = [{ type: 'SOLID', color: WHITE }];
section.resizeWithoutConstraints(bboxW + 2*SECTION_PAD, bboxH + HEADER_BLOCK + 2*SECTION_PAD);
section.x = 832;
section.y = 152;
const font = { family: 'Inter', style: 'Medium' };
await figma.loadFontAsync(font);
const h2 = figma.createText();
h2.fontName = font;
h2.fontSize = 40;
h2.characters = "Target State Architecture";
h2.fills = [{ type: 'SOLID', color: CHARCOAL }];
h2.textAutoResize = 'WIDTH_AND_HEIGHT';
section.appendChild(h2);
h2.x = SECTION_PAD;
h2.y = SECTION_PAD;
const dxBase = -minX + SECTION_PAD;
const dyBase = -minY + SECTION_PAD + HEADER_BLOCK;
for (const n of diagramNodes) {
const newLocalX = n.x + dxBase;
const newLocalY = n.y + dyBase;
section.appendChild(n);
n.x = newLocalX;
n.y = newLocalY;
}
section.placeholder = false;CRITICAL TRAP — Connector re-routing after reparent (delete + recreate is the reliable fix)
Setting n.x/n.y on a CONNECTOR node during reparent "pins" its bounding box to bogus coordinates and breaks its auto-routing.
The cheap fix (`c.connectorStart = c.connectorStart`) is NOT reliable for long-bend connectors. Empirical finding: short connectors (single-bend, both endpoints close) re-route fine via the assign-to-self trick. Long-bend connectors that span large distances (e.g. service → datastore across multiple lanes) retain stale elbow waypoints and end up with negative y-coordinates, extending hundreds of pixels above the section.
Reliable fix: delete every connector and recreate from spec. This is the STRICT default for diagram reparenting in this skill.
const fontMedium = { family: 'Inter', style: 'Medium' };
await figma.loadFontAsync(fontMedium);
const connectors = section.children.filter(c => c.type === 'CONNECTOR');
// 1. Capture each connector's full spec BEFORE deletion
const specs = connectors.map(c => ({
start: { endpointNodeId: c.connectorStart.endpointNodeId, magnet: c.connectorStart.magnet || 'AUTO' },
end: { endpointNodeId: c.connectorEnd.endpointNodeId, magnet: c.connectorEnd.magnet || 'AUTO' },
lineType: c.connectorLineType,
strokes: JSON.parse(JSON.stringify(c.strokes)),
strokeWeight: c.strokeWeight,
dashPattern: c.dashPattern ? Array.from(c.dashPattern) : null,
startStrokeCap: c.connectorStartStrokeCap,
endStrokeCap: c.connectorEndStrokeCap,
// Label: prefer text.characters, fall back to name (the Mermaid label is in name)
label: (c.text && c.text.characters) ? c.text.characters : c.name,
name: c.name,
}));
// 2. Delete the broken connectors
for (const c of connectors) c.remove();
// 3. Recreate fresh inside the section
for (const s of specs) {
const c = figma.createConnector();
section.appendChild(c);
c.connectorStart = s.start;
c.connectorEnd = s.end;
if (s.lineType) c.connectorLineType = s.lineType;
if (s.strokes) c.strokes = s.strokes;
if (s.strokeWeight) c.strokeWeight = s.strokeWeight;
if (s.dashPattern && s.dashPattern.length) c.dashPattern = s.dashPattern;
if (s.startStrokeCap) c.connectorStartStrokeCap = s.startStrokeCap;
if (s.endStrokeCap) c.connectorEndStrokeCap = s.endStrokeCap;
c.name = s.name;
if (s.label) {
// STRICT — fresh connector text has NO usable defaults. Set ALL THREE explicitly:
c.text.fontName = fontMedium; // (1) loaded font
c.text.fontSize = 14; // (2) labels render at 14px
c.text.characters = s.label; // (3) the label
c.text.fills = [{ type: 'SOLID', color: CHARCOAL }]; // (4) text color — DEFAULT IS EMPTY ARRAY
}
}Why the assign-to-self trick alone fails
c.connectorStart = c.connectorStart triggers a re-route, but the elbow control points (the bend waypoints) are cached separately and don't reset. For an L-shaped or U-shaped connector, the cached waypoint may sit hundreds of pixels away from the new endpoint positions, leaving the connector visually broken with the bbox extending outside the section.
figma.createConnector() produces a connector with no cached waypoints — the routing recomputes from scratch based on the current endpoint positions. That's why delete + recreate is reliable.
Alternative — never set x/y on connectors during the move loop
If you skip CONNECTORs entirely in the reparent x/y-setting loop and only call appendChild + the assign-to-self trick, short connectors will work. Long-bend connectors will still break. Delete + recreate covers both cases.
Connector text — fontName + fontSize + fills are ALL mandatory
A freshly-created connector's text sublayer is a stub:
fontNameis set to a default that may not be loaded — you must reassign explicitly to a loaded font.fontSizeis missing/zero — set to 14 explicitly.fillsis an empty array `[]` — the label renders with NO COLOR (transparent) and is invisible. Settext.fills = [{ type: 'SOLID', color: CHARCOAL }]explicitly.
If you set only characters and fontName, the label will appear correctly when you read c.text.characters back, BUT will not render visually. This is the easiest-to-miss bug in this whole skill — verify with a screenshot, not a use_figma read.
The four-line pattern (with comments) belongs at every connector creation site:
c.text.fontName = { family: 'Inter', style: 'Medium' }; // already loaded
c.text.fontSize = 14;
c.text.characters = label;
c.text.fills = [{ type: 'SOLID', color: { r: 0x1E/255, g: 0x1E/255, b: 0x1E/255 } }];Failure handling
If generate_diagram fails: do NOT leave the section with placeholder = true. Instead, in the reparent call, create a TEXT node explaining the failure and treat it as the diagram:
const errorText = figma.createText();
errorText.fontName = font;
errorText.fontSize = 16;
errorText.characters = "Diagram generation failed: " + errMessage + ". Regenerate manually.";
errorText.fills = [{ type: 'SOLID', color: CHARCOAL }];
section.appendChild(errorText);
errorText.x = 32;
errorText.y = headerBottom + 16;
errorText.resize(RIGHT_COL_W_MIN - 64, errorText.height);
section.resizeWithoutConstraints(RIGHT_COL_W_MIN, errorText.y + errorText.height + 32);
section.placeholder = false;Continue to the next diagram.
Pre-flight checklist
- [ ] Skeleton section created with white fill + H2 header.
- [ ]
generate_diagramcalled with the correcteditorType: "figjam"and architecture layout code. - [ ] Diagram reparented with
appendChildBEFORE settingx/y. - [ ] Section resized to
max(1200, diag.width + 64) × (diag.y + diag.height + 32). - [ ]
placeholder = falseat end of reparent call. - [ ] Failure leaves a text placeholder, not an empty section.
- [ ] Return
section.id+diag.id(orerrorText.id) inmutatedNodeIds.
Block: Intro callout
A highlighted intro paragraph rendered at the top of a section, with a distinct background fill that stands out from the section's own palette color. Used to orient the reader before the rest of the section's content.
When to use
- Motivation section — a one- or two-sentence "why this project exists" paragraph before the "Resources" subsection.
- Any section where the first block is a short, high-visibility statement (mission, scope note, TL;DR).
Do NOT use for:
- Standalone body text (use text-primitives.md).
- Multi-paragraph intros (they dilute the highlight).
Structure
A FRAME (with auto-layout OFF — just a background rect) containing a single body TEXT node, positioned just below the H2 header inside the section.
Alternative: a plain TEXT node with its own fill/padding via setRangeFills — but FRAME is simpler.
Geometry
- Width: inner section width =
LEFT_COL_W - 2 * SECTION_PAD = 736. - Padding: 16px on all sides.
- Text: body (16px), wrapped at
frame.width - 32. - Background fill: a softer/lighter variant of the parent section palette. For
lightVioletparent → useSECTION.lightVioletwith a lower-opacity overlay, or a related lighter purple. Simplest: a white-ish overlay over the parent color (fill =SECTION.white).
Create script
const font = { family: 'Inter', style: 'Medium' };
await figma.loadFontAsync(font);
// Position after H2
const h2 = section.children.find(c => c.type === 'TEXT' && c.fontSize === 40);
const calloutY = (h2 ? h2.y + h2.height : 32) + 16;
const frame = figma.createFrame();
frame.name = "Intro callout";
frame.fills = [{ type: 'SOLID', color: SECTION.white }];
frame.cornerRadius = 8;
section.appendChild(frame);
frame.x = 32;
frame.y = calloutY;
const INNER_W = 800 - 2 * 32; // 736 — inner width of a left-column section
frame.resize(INNER_W, 100); // temp; resize after text measured
const text = figma.createText();
text.fontName = font;
text.fontSize = 16;
text.characters = introText;
text.fills = [{ type: 'SOLID', color: CHARCOAL }];
text.textAutoResize = 'HEIGHT';
frame.appendChild(text);
text.x = 16;
text.y = 16;
text.resize(INNER_W - 32, text.height);
// Hug frame around text + padding
frame.resize(INNER_W, text.height + 32);Color variants
| Parent slug | Callout fill | Reason |
|---|---|---|
motivation (lightViolet) | SECTION.white | Max contrast — callout pops off the purple bg |
context (lightGray) | SECTION.white | Same — white on gray |
goals (lightGreen) | SECTION.white | Same |
rollout (lightOrange) | SECTION.lightYellow | Warm-on-warm, subtle |
| any | Custom color the user specifies | Override |
Default: white if parent is a mid-tone palette; a closely related lighter palette if parent is already very light.
Pre-flight checklist
- [ ] Callout is a
FRAME(not a SECTION) — nested sections are handled separately. - [ ] Frame appended to section BEFORE setting
x/y. - [ ] Frame fill uses
hex/255palette color. - [ ] Text is appended to FRAME (not the section).
- [ ] Frame height is hugged to text height + 32.
- [ ] Frame is part of
mutatedNodeIdsreturn.
Block: Metadata strip
The H1 project title + 4 body texts (Owner / Status / Last updated / Source) at the top of the column wrapper. NOT a standalone section, NOT positioned at the page origin — they are the first children of the unlabeled white column wrapper SECTION.
Structure
Not a section — just five TEXT nodes positioned at the page top starting at (0, 0).
| Element | Font size | Position (local to column wrapper) |
|---|---|---|
| Project title (H1) | 40 Inter Medium | (64, 64) — column wrapper padding |
| Owner | 16 | At (64, h1.y + h1.height + 16) — start of body row |
| Status | 16 | Right of Owner with 32px gap |
| Last updated | 16 | Right of Status with 32px gap |
| Source PRD link | 16, underlined | Right of Last updated with 32px gap |
H1 is 40px (NOT 64). 64 doesn't fit inside the 800-wide column wrapper for typical project titles. 40 matches H2 size visually but the placement at the top of the wrapper makes it read as the title.
Total metadata-block height (top of wrapper to bottom of body row): 64 + 48 + 16 + 19 = 147 (with 64 padding above, then 64 padding below the body row → first section starts at y = 211).
Positioning logic (children of the column wrapper)
const font = { family: 'Inter', style: 'Medium' };
await figma.loadFontAsync(font);
const PAD = 64; // column wrapper padding
// H1 project title — first child of the column wrapper at (PAD, PAD)
const title = figma.createText();
title.fontName = font;
title.fontSize = 40; // STRICT — 40, not 64
title.characters = projectTitle;
title.fills = [{ type: 'SOLID', color: CHARCOAL }];
title.textAutoResize = 'WIDTH_AND_HEIGHT';
columnWrapper.appendChild(title);
title.x = PAD;
title.y = PAD;
// Body row — Owner / Status / Last updated / PRD link, 32px gap
const rowY = title.y + title.height + 16;
const labels = [
{ text: "Owner: " + ownerName },
{ text: "Status: Draft" },
{ text: "Last updated: " + new Date().toISOString().slice(0, 10) },
{ text: "Source: " + prdPath, underline: true },
];
let rowX = 0;
const createdIds = { title: title.id };
const rowIds = {};
for (const [i, item] of labels.entries()) {
const t = figma.createText();
t.fontName = font;
t.fontSize = 16;
t.characters = item.text;
t.fills = [{ type: 'SOLID', color: CHARCOAL }];
if (item.underline) {
t.setRangeTextDecoration(0, t.characters.length, 'UNDERLINE');
}
t.textAutoResize = 'WIDTH_AND_HEIGHT';
columnWrapper.appendChild(t);
t.x = rowX;
t.y = rowY;
rowX += t.width + 32;
rowIds["row" + i] = t.id;
}
// First content section starts at: rowY + body.height + 64 (gutter)
return { createdNodeIds: { title: title.id, ...rowIds } };Notes:
- The nodes are children of the column wrapper SECTION, not the page.
- They sit at the top of the wrapper (above the 6 content sections), with the same 64px padding as the rest of the wrapper.
- No separate background fill — they inherit the wrapper's white fill.
Status values
"Draft"— initial"In review""Approved""In progress""Shipped"
Always start with "Draft".
Missing fields
- No owner →
"Owner: TBD" - No PRD path → omit the PRD link entirely; shrink the row.
Last updated— always set at generation time.
Pre-flight checklist
- [ ] Font loaded before any text creation.
- [ ] All five (or four if PRD missing) text nodes created.
- [ ] H1 at
(0, 0); body row aty = title.height + 16. - [ ] PRD link has underline via
setRangeTextDecoration. - [ ] Return all node IDs in
createdNodeIds.
Block: Multi-column text
Side-by-side text columns with per-column titles, body text, and (optional) bulleted list. Visual dividers separate columns.
When to use
- Comparing options (
Option 1: Webhook,Option 2: Polling,Option 3: GH Actions,Option 4: Hybrid). - Parallel facets of a decision (Pros | Cons | Neutral).
- Any case where N parallel blocks need to sit in one row and wrap the same kind of content.
Do NOT use for:
- Tabular rows-across-columns → use table.md.
- A single bullet list → use text-primitives.md.
- Grouping different kinds of content → use nested-section.md.
Supported variants
- 2 columns: wider per column; works for Pros/Cons.
- 3 columns: balanced; works for common decision matrices.
- 4 columns: tight per column; caps at this. More than 4 → switch to a table or a second row.
Geometry (inside an 800-wide left section)
| Cols | Inner width | Gutters | Col width | X positions |
|---|---|---|---|---|
| 2 | 736 | 1 × 32 | 352 | 32, 416 |
| 3 | 736 | 2 × 32 | 224 | 32, 288, 544 |
| 4 | 736 | 3 × 32 | 160 | 32, 224, 416, 608 |
Formula:
innerW = 800 - 2*32 = 736
colW = (innerW - (N-1)*32) / N
colX[i] = 32 + i * (colW + 32)Structure (per column)
Each column is a vertical stack of three TEXT nodes appended to the parent section:
1. Column title — H3-like (24px) or body-bold (16px bold — not currently on palette; use 24px). 2. Column body — body text (16px), wrapped at colW. 3. Column list — bulleted list (16px), wrapped at colW.
Divider between columns
A thin LINE node between each pair of adjacent columns, at x = column boundary, y from title-top to bottom of tallest column.
const divider = figma.createLine();
divider.name = "Column divider";
divider.strokes = [{ type: 'SOLID', color: h(0xE6, 0xE6, 0xE6) }]; // light gray
divider.strokeWeight = 1;
section.appendChild(divider);
divider.x = colX[i] - 16; // between column i-1 and i
divider.y = titlesTopY;
divider.resize(1, tallestColHeight);Alternative (simpler): skip dividers — the whitespace between columns is enough. Use dividers only when the user asked for explicit separation.
Create script (3-column example)
const font = { family: 'Inter', style: 'Medium' };
await figma.loadFontAsync(font);
const N = 3;
const innerW = 800 - 2*32;
const colW = (innerW - (N-1)*32) / N; // 224
const colX = i => 32 + i * (colW + 32);
const columns = [
{ title: "Option 1: Webhook", body: "…description…", bullets: ["Pro 1", "Pro 2"] },
{ title: "Option 2: Polling", body: "…", bullets: ["Pro 1"] },
{ title: "Option 3: GH Actions", body: "…", bullets: ["Pro 1", "Pro 2"] },
];
// Position below the H2 header (and any preceding body)
const startY = prevChildBottom + 16;
const allIds = [];
for (let i = 0; i < N; i++) {
const col = columns[i];
let y = startY;
// Title
const title = figma.createText();
title.fontName = font;
title.fontSize = 24;
title.characters = col.title;
title.fills = [{ type: 'SOLID', color: CHARCOAL }];
title.textAutoResize = 'HEIGHT';
section.appendChild(title);
title.x = colX(i);
title.y = y;
title.resize(colW, title.height);
y += title.height + 16;
// Body
const body = figma.createText();
body.fontName = font;
body.fontSize = 16;
body.characters = col.body;
body.fills = [{ type: 'SOLID', color: CHARCOAL }];
body.textAutoResize = 'HEIGHT';
section.appendChild(body);
body.x = colX(i);
body.y = y;
body.resize(colW, body.height);
y += body.height + 16;
// Bulleted list
if (col.bullets && col.bullets.length) {
const listText = col.bullets.join('\n');
const list = figma.createText();
list.fontName = font;
list.fontSize = 16;
list.characters = listText;
list.fills = [{ type: 'SOLID', color: CHARCOAL }];
list.textAutoResize = 'HEIGHT';
list.setRangeListOptions(0, listText.length, { type: 'UNORDERED' });
list.setRangeIndentation(0, listText.length, 1);
section.appendChild(list);
list.x = colX(i);
list.y = y;
list.resize(colW, list.height);
y += list.height;
}
allIds.push(title.id, body.id);
}
// Compute the bottom of the tallest column for the next element's startY.
const multiColBottom = Math.max(...section.children
.filter(c => c.y >= startY)
.map(c => c.y + c.height));Use inside a nested section
If the columns sit inside a child SECTION (as in the v1 board's "Design Decisions" → "Decision 1" → 4-col options), replace section with the child section node and adjust the geometry to the child's inner width (child.width - 64).
Pre-flight checklist
- [ ] N is 2, 3, or 4. More → switch to a table or stack in two rows.
- [ ] Column width uses
(innerW - (N-1)*32) / Nformula. - [ ] Every text node: font loaded,
appendChildbeforex/y,textAutoResize='HEIGHT'+resize(colW, height)for wrapping. - [ ] Columns all start at the same
y(aligned tops). - [ ] After all columns are placed, compute
multiColBottomfor the next element. - [ ] Text node IDs returned in
mutatedNodeIds.
Block: Nested section
A SECTION inside another SECTION. Used to group related sub-content (e.g. a "Design Decisions" parent section containing three "Decision 1/2/3" child sections). 1 level deep only — no grandchildren.
When to use
- The parent concept has multiple sibling items each with their own structured content (headers, tables, bullet lists).
- Example from the v1 board:
"Design Decisions: 1, 2, & 3"parent →"Design Decision 1: Delivery Surfaces","Design Decision 2: Trigger Mechanism", etc. child sections, each with its own tables and multi-column option layouts.
Do NOT nest for:
- Single paragraphs with a subheading → use an H3 from text-primitives.md.
- Option comparison → use multi-column-text.md.
Structure
Parent SECTION (color = section palette color for parent slug)
├─ H2 header (parent title, e.g. "Design Decisions: 1, 2, & 3")
├─ Child SECTION 1 (color = same palette but lighter or matching shade)
│ ├─ H3 header
│ ├─ body text
│ └─ table / multi-column / …
├─ Child SECTION 2
└─ Child SECTION 3Color rule
Child sections inherit the parent's palette color by default. For visual separation, override to:
- Same palette, same hex (looks like embedded content).
- White (
SECTION.white) for a high-contrast "card" look. - A related palette (e.g. parent
lightBlue→ childlightBluebut with a white inner content block, or parentlightViolet→ childlightBlueif decisions are categorized).
Default: match parent for a cohesive look; override per child if the content visually needs distinction.
Sizing
- Parent section width = 800 (left column).
- Child sections: width =
800 - 2*32 = 736(full inner width of parent minus padding). - Child section height = hug its own children + 32 padding.
- Child section x inside parent = 32. y = previous child's bottom + 32.
- Parent section height = 32 + H2.height + 32 + Σ(child.height + 32) - 32 + 32.
Create script (inside Step 7 fill pass for the parent)
// Parent is already created in skeleton pass; fetch by ID.
const parent = await figma.getNodeByIdAsync(parentSectionId);
// H2 header already appended as first child; compute y for first child section.
const h2 = parent.children.find(c => c.type === 'TEXT');
let childY = (h2 ? h2.y + h2.height : 32) + 32;
const childSpecs = [
{ name: "Decision 1", header: "Design Decision 1: Delivery Surfaces" },
{ name: "Decision 2", header: "Design Decision 2: Trigger Mechanism" },
// …
];
const font = { family: 'Inter', style: 'Medium' };
await figma.loadFontAsync(font);
const childIds = [];
for (const spec of childSpecs) {
const child = figma.createSection();
child.name = ""; // STRICT — no section label; H3 inside is the only title
child.fills = [{ type: 'SOLID', color: SECTION.lightBlue }]; // or same as parent
child.resizeWithoutConstraints(736, 300); // temp height; overwritten after filling
parent.appendChild(child);
child.x = 32;
child.y = childY;
// child H3 header (using H3 semantic — 24px)
const h3 = figma.createText();
h3.fontName = font;
h3.fontSize = 24;
h3.characters = spec.header;
h3.fills = [{ type: 'SOLID', color: CHARCOAL }];
child.appendChild(h3);
h3.x = 32;
h3.y = 32;
// TODO: append body / tables / multi-column content for this child
// hug child after its content is added
const childBottom = Math.max(...child.children.map(c => c.y + c.height));
child.resizeWithoutConstraints(736, childBottom + 32);
childY += child.height + 32;
childIds.push(child.id);
}
// re-hug parent
const parentBottom = Math.max(...parent.children.map(c => c.y + c.height));
parent.resizeWithoutConstraints(800, parentBottom + 32);Pre-flight checklist
- [ ] Parent and child are BOTH
SECTIONnodes (notFRAME). - [ ] Both parent and child have
name = ""(STRICT — no section labels). - [ ] Child is appended to parent BEFORE setting
child.x/child.y. - [ ] Only 1 level of nesting. No grandchildren.
- [ ] Parent is re-resized AFTER all children are hugged.
- [ ] Both parent and child have
placeholder = falseat end of fill. - [ ] Return includes
parent.idand everychild.idinmutatedNodeIds/createdNodeIds.
Block: Top-level section
A FigJam Section node with a colored background, an H2 header as first child, 32px padding, and children laid out below the header.
Definition
- Node type:
SECTION(created withfigma.createSection()). - Width: left column = 800, right column = ≥1200 (driven by diagram width).
- Fill:
section.fills = [{ type: 'SOLID', color: SECTION_COLOR_BY_SLUG[slug] }]. See foundation/palette.md. - STRICT — no section label:
section.name = "". Project-plan boards do not use FigJam's section title-bar label. The user-facing title is rendered as the H2 text node inside the section (see Header below). Settingsection.nameto anything non-empty produces a duplicate label that visually clutters the board. - Header: an H2 text node as the section's first child, at
(32, 32). Header text is the user-facing title (e.g."Goals, Non-Goals & Success Metrics"). This is the only label the section gets. - Padding: 32px on all four sides. First child at
(32, 32); last child's bottom + 32 = section height. - Hug behavior: sections do NOT auto-grow. Call
section.resizeWithoutConstraints(w, h)after appending children. - Placeholder during build: set
section.placeholder = truein the skeleton pass. Setsection.placeholder = falseat the end of the fill pass for that section.
Create script (skeleton pass)
const section = figma.createSection();
section.name = ""; // STRICT — no FigJam section label; H2 text inside is the only title
section.fills = [{ type: 'SOLID', color: SECTION_COLOR_BY_SLUG.goals }];
section.resizeWithoutConstraints(800, 400); // DEFAULT_H — fill pass will overwrite
section.x = 0;
section.y = 152 + cumulative_y;
section.placeholder = true;Add the H2 header (first action in fill pass)
const font = { family: 'Inter', style: 'Medium' };
await figma.loadFontAsync(font);
const h2 = figma.createText();
h2.fontName = font;
h2.fontSize = 40;
h2.characters = "Goals, Non-Goals & Success Metrics"; // user-facing header
h2.fills = [{ type: 'SOLID', color: CHARCOAL }];
section.appendChild(h2); // append FIRST
h2.x = 32;
h2.y = 32;Hug section after filling
After all children are appended and positioned:
const maxBottom = Math.max(...section.children.map(c => c.y + c.height));
section.resizeWithoutConstraints(800, maxBottom + 32);
section.placeholder = false;Pre-flight checklist
- [ ]
section.name = ""(no section title-bar label — STRICT). - [ ] Fill is set via
hex/255palette color (not rounded decimals). - [ ] H2 is the first child.
- [ ]
appendChildis called before settingx/yon any child. - [ ]
resizeWithoutConstraints(w, h)runs after all children are appended. - [ ]
section.placeholderisfalseat end of this section's fill. - [ ] Return includes
section.idinmutatedNodeIds(orcreatedNodeIdson skeleton pass).
Right-column (diagram) section
Same as above but:
fill=ARCH_PALE.white.- Width =
max(RIGHT_COL_W_MIN, diag.width + 64)after the diagram is reparented. section.name = ""(same rule — no title-bar label).- See blocks/diagram-section.md for the reparent flow.
Block: Sticky column
A vertical stack of stickies, one statement per sticky. Used for success metrics, risks, open questions — anything that's a "pile of discrete cards" where readers can add more.
When to use
- Success Metrics (yellow stickies, one metric per sticky).
- Risks (light red / pink stickies).
- Open Questions (light blue stickies).
- Dependencies list when the user wants them as "cards" instead of a table.
Sticky facts
figma.createSticky()creates a STICKY node. FigJam only.- Sticky widths are fixed: 240 (default) or 416 (wide mode via
sticky.isWideWidth = true). - Sticky heights are determined by content — you cannot resize them directly. Text that overflows 240×240 pushes the sticky taller. Need to measure after creating.
sticky.textis a text sublayer — load its font before settingcharacters.sticky.fillsis NOT how you color stickies. Usesticky.background = STICKY.<color>— wait, actually check the API. The create-sticky.md reference uses a color preset map.
Actually stickies have a fills array that sets the color like any shape. See the figma-use-figjam skill's create-sticky reference for the exact property name at runtime — the v1 skill used sticky.fills.
Two-pass layout
You MUST create all stickies, then measure, then position. Assuming a uniform 240×240 will cause overlaps when text wraps to multiple lines.
const font = { family: 'Inter', style: 'Medium' };
await figma.loadFontAsync(font);
// Pass 1: create all stickies with text + color; don't position yet.
const stickies = [];
const items = ["p95 < 200ms", "≥ 80% users rate usable", "Error rate < 0.5%"];
for (const label of items) {
const s = figma.createSticky();
s.text.fontName = font;
s.text.characters = label;
s.fills = [{ type: 'SOLID', color: STICKY.yellow }]; // use sticky palette
section.appendChild(s);
stickies.push(s);
}
// Pass 2: now read actual heights and stack vertically.
const STICKY_GAP = 32; // vertical gap between stickies
let y = prevChildBottom + 16; // below H2 or previous block
for (const s of stickies) {
s.x = 32;
s.y = y;
y += s.height + STICKY_GAP;
}
// Return this y as the new bottom for the next block
const stickyColumnBottom = y - STICKY_GAP;Column positioning inside a multi-column layout
If the sticky column is ONE column in a 3- or 4-column split (e.g. Success Metrics as column 3 of Goals), use colX(2) from multi-column-text.md as the sticky x.
Sticky width is 240 — if colW < 240, the sticky will overflow the column. For narrow columns:
- Either use
isWideWidth = trueif the column is 416+ wide. - Or accept that stickies visually overlap the gutter — 240 is the minimum.
- For 3-col in an 800 section (colW=224), stickies are slightly too wide. Switch to a 2-col layout or use a non-sticky list.
Color rules
Match the sticky color to the semantic of the section:
- Success metrics → yellow
- Risks → red or pink
- Open questions → blue
- Dependencies → orange
- Blockers (highlighted) → red
- General notes → white or gray
Pre-flight checklist
- [ ] Font loaded before setting
sticky.text.characters. - [ ] Color via
sticky.fillswith ahex/255palette color from the STICKY (not SECTION) palette. - [ ] All stickies appended BEFORE positioning (two-pass layout).
- [ ] Actual
.heightread after creation — do not assume 240×240. - [ ] Stickies stack with 32px gap.
- [ ] Return all sticky IDs in
mutatedNodeIds.
Block: Table
A FigJam TABLE node with a colored header row whose palette inherits from the parent section.
When to use
- Resources (columns: Type, Link, Description)
- Goals / Non-goals (columns: Goal, Description)
- Metrics (columns: Type, Metric)
- Dependencies (columns: Type, Dependency, Notes)
- Rollout stages (columns: Stage, Activities, Metric to Watch, Gate)
- Milestones (columns: #, Phase, Timeline, Description)
- Tradeoffs (inside a nested Design Decision section)
Use a table when the data is inherently structured rows × columns and every row answers the same set of questions. Otherwise prefer body text or multi-column text.
Key API facts
- FigJam only (
figma.createTable(rows, cols)). Not available in Figma Design. table.widthandtable.heightare read-only. UseresizeRow(i, h)andresizeColumn(j, w).- Rows / columns cannot go below their minimum size.
table.cellAt(r, c)returns aTABLE_CELLnode with atextsublayer. Load the cell's font before settingtext.charactersortext.fills.- Tables do NOT have strokes. Cells have fills only.
Palette inheritance — STRICT (two-tone, light-on-pale)
The header row uses the FigJam SECTION palette color matching the parent section's hue. NOT the dark-saturated palette. See TABLE_HEADER_BY_SECTION in foundation/palette.md.
goalssection bg#EBFFEE(ARCH_PALE.green) → header fill#CDF4D3(lightGreen) +#1E1E1Echarcoal textdependenciessection bg#FFF7F0(ARCH_PALE.orange) → header fill#FFE0C2(lightOrange) + charcoal textmotivationsection bg#F8F5FF(ARCH_PALE.violet) → header fill#DCCCFF(lightViolet) + charcoal text- Body cells: default fill (white). Text:
#1E1E1E(charcoal).
Both header AND body cells use `Inter Bold` 16px. This matches the reference board's table convention.
Create script
// rows[0] is the header row.
const rows = [
['Type', 'Link', 'Description'],
['PRD', 'https://…', 'Markdown → FigJam sync'],
['Design doc', 'https://…', 'Architecture proposal'],
];
const numRows = rows.length;
const numCols = rows[0].length;
const table = figma.createTable(numRows, numCols);
// STRICT: load Inter Bold (NOT Medium) — both header and body cells use Bold
const fontBold = { family: 'Inter', style: 'Bold' };
await figma.loadFontAsync(fontBold);
// Palette lookup based on parent section
const headerPreset = TABLE_HEADER_BY_SECTION[sectionSlug]; // e.g. TABLE_HEADER.lightGreen
const CHARCOAL_FILL = { type: 'SOLID', color: CHARCOAL };
for (let r = 0; r < numRows; r++) {
for (let c = 0; c < numCols; c++) {
const cell = table.cellAt(r, c);
cell.text.fontName = fontBold; // STRICT: Bold for every cell
cell.text.fills = [CHARCOAL_FILL]; // STRICT: charcoal text in every cell
if (r === 0) {
cell.fills = [{ type: 'SOLID', color: headerPreset.fill }]; // light hue
}
cell.text.characters = rows[r][c];
}
}
// Append to section and position (appendChild BEFORE x/y)
section.appendChild(table);
table.x = 32;
table.y = prevChildBottom + 16;
// Width: table is content-sized; columns can be resized explicitly if needed.
// For a standard 3-col table inside an 800-wide section with 32 padding:
// target total width = 736, so each column ≈ 245. Adjust based on data.
table.resizeColumn(0, 160);
table.resizeColumn(1, 240);
table.resizeColumn(2, 336);Column width heuristics
| N columns | Target total width | Distribution |
|---|---|---|
| 2 | 736 | 240 / 496 (label / description) |
| 3 | 736 | 160 / 240 / 336 (short / medium / long) |
| 4 | 736 | 120 / 180 / 180 / 256 |
| 5 | 736 | 100 / 140 / 160 / 160 / 176 |
Adjust if one column clearly needs more room (e.g. Description always wider than #).
Ordered / numbered rows
For Milestones (e.g. numbered # column), the first column's values are "1", "2", etc. — just strings. No list formatting needed.
Multi-line cell text
Cells wrap automatically based on the column width. Use \n in text.characters for explicit line breaks.
Pre-flight checklist
- [ ] Table created with exact
(numRows, numCols)matching the data. - [ ] `Inter Bold` loaded BEFORE setting any cell text (NOT Medium — STRICT).
- [ ] Header row has explicit
cell.fills(light-tone matching section) + dark charcoalcell.text.fills. - [ ] Body cells have
cell.text.fills = CHARCOALand no explicitcell.fills(default white). - [ ] Every cell's
text.fontName = { family: 'Inter', style: 'Bold' }. - [ ] Palette colors use
hex/255notation. - [ ] Appended to section BEFORE setting
x/y. - [ ] Columns resized if defaults don't match the content.
- [ ] Table ID returned in
mutatedNodeIds.
Block: Text primitives (body, H3, bulleted list)
The text elements that appear inside almost every section.
Typography constants — STRICT (matches reference board)
| Element | When to use | Font | Size | Color |
|---|---|---|---|---|
| H1 | Board metadata strip | Inter Medium | 64 | Charcoal #1E1E1E |
| H2 | Section title (top of every section) | Inter Medium | 40 | Charcoal #1E1E1E |
| H3 — full-width subhead | Sub-section header in single-column context (e.g. "Resources" inside Motivation) | Inter Medium | 40 | Charcoal #1E1E1E |
| H3 — nested-section header | First text inside a child section (e.g. "Design Decision 1: Delivery Surface") | Inter Medium | 32 | Charcoal #1E1E1E |
| H3 — column title | Title of a column in a 2/3/4-col multi-column-text layout (e.g. "Goals", "Risks", "Open Questions") | Inter Medium | 24 | Charcoal #1E1E1E |
| Body | Paragraphs, list items | Inter Medium | 16 | Charcoal #1E1E1E |
| Table cell | Header AND body cells | Inter Bold | 16 | Header text + body text both Charcoal #1E1E1E |
Why three different H3 sizes? The reference board uses 40px for sub-section heads inside an otherwise full-width section (matches H2 visually for emphasis). Column titles in narrow contexts must be 24px so they don't wrap or overflow. Nested-section headers sit between — 32px reads as a section header without overflowing the child section.
All text uses the same font family and style. Size is the only variable for headers.
Load the font ONCE per `use_figma` script, before creating any text:
const font = { family: 'Inter', style: 'Medium' };
await figma.loadFontAsync(font);Body paragraph
const body = figma.createText();
body.fontName = font;
body.fontSize = 16;
body.characters = BODY_TEXT;
body.fills = [{ type: 'SOLID', color: CHARCOAL }];
body.textAutoResize = 'HEIGHT';
section.appendChild(body);
body.x = 32;
body.y = h2.y + h2.height + 16;
body.resize(800 - 64, body.height); // wrap at inner width = 736H3 subheader — pick the size for the context
// Full-width subhead inside a single-column section (e.g. "Resources" in Motivation)
const h3Full = figma.createText();
h3Full.fontName = font;
h3Full.fontSize = 40; // STRICT: 40, NOT 24
h3Full.characters = "Resources";
h3Full.fills = [{ type: 'SOLID', color: CHARCOAL }];
section.appendChild(h3Full);
h3Full.x = 32;
h3Full.y = body.y + body.height + 24;
// Nested-section header (e.g. "Design Decision 1: Delivery Surface")
h3Nested.fontSize = 32; // STRICT: 32 (fits in 736-wide child section)
// Column title in a multi-column layout (e.g. "Goals", "Risks", "Open Questions")
h3Col.fontSize = 24; // STRICT: 24 (fits in 224-px columns without wrapping)Bulleted list
FigJam text supports list formatting via setRangeListOptions. Each line is a list item. Load font first.
const items = ["Item one", "Item two", "Item three"];
const text = items.join('\n');
const list = figma.createText();
list.fontName = font;
list.fontSize = 16;
list.characters = text;
list.fills = [{ type: 'SOLID', color: CHARCOAL }];
list.textAutoResize = 'HEIGHT';
list.setRangeListOptions(0, text.length, { type: 'UNORDERED' });
list.setRangeIndentation(0, text.length, 1);
section.appendChild(list);
list.x = 32;
list.y = h3.y + h3.height + 16;
list.resize(800 - 64, list.height);Ordered list
Same as bulleted, but type: 'ORDERED'.
list.setRangeListOptions(0, text.length, { type: 'ORDERED' });Stacking rule — STRICT 24px between blocks
Every gap between sibling blocks inside a section is 24px. No exceptions. Use prevChild.y + prevChild.height + 24.
| From | To | Gap |
|---|---|---|
| Section top | H2 (first child) | 32 (= section padding) |
| H2 | body / intro / H3 / table / first column | 24 |
| Body | H3 | 24 |
| H3 (40/32/24px) | body / table / list / column | 24 |
| Body | body | 24 |
| List | next block | 24 |
| Last child | Section bottom | 32 (= section padding) |
NEVER use a fixed offset. Read prevChild.height after the font size is set, then add 24. If you later change a header's fontSize, you must re-position every downstream child or they'll overlap.
Pre-flight checklist
- [ ] Font loaded before any
characters/fontSize/fillson text nodes. - [ ] Every text node appended to its parent BEFORE setting
x/y/resize. - [ ]
textAutoResize = 'HEIGHT'+resize(w, text.height)for wrapped body / list nodes. H2/H3 usetextAutoResize = 'WIDTH_AND_HEIGHT'or skip resize if they fit on one line. - [ ] Return text node IDs in
mutatedNodeIds.
Codebase grounding
Hybrid strategy: user provides entry points, skill expands with bounded depth. Runs between interview phase A and phase B.
Input: entry points from the user
Collected during interview phase A. User supplies any combination of:
- File paths — absolute or relative, e.g.
share/mermaid/src/processors/edge_routing.ts. - Directory paths — e.g.
share/mermaid/src/processors/. - Globs — e.g.
web/src/components/diagrams/**/*.ts. - Service names — mapped via repo conventions (e.g. a known service manifest or
services/directory). - Doc paths —
ARCHITECTURE.md,docs/*.md,README.mdin a relevant directory.
If the user says "none" or "skip", skip grounding entirely and proceed to interview phase B. Architecture diagrams will be drawn from PRD text alone.
Expansion rules
For each entry point, up to the 20-file cap:
1. Read the file. Use Read (or Glob first for a dir/glob, then Read on selected files). 2. Extract imports. Collect static import paths (TS/JS import, Ruby require/require_relative, Python import, Go import, etc.). Resolve to in-repo paths where possible. 3. Follow 1 level of imports into the same repo. Do NOT chase transitively (depth > 1). External package imports are noted but not opened. 4. Read adjacent architecture docs. For each file, walk up the directory tree to the repo root looking for CLAUDE.md, README.md, ARCHITECTURE.md, DESIGN.md. Read any found. This provides architectural context. 5. Extract OWNERSHIP / OWNERS file if present in any ancestor directory (Figma repo has these). Capture the team name.
Cap
- 20 files total. Hard limit. Counts every
Readinvocation. - When the cap is hit, stop. Record in the tech-context:
expansion_truncated: true, files_read_count: 20. The content plan printout surfaces this so the user knows grounding was incomplete.
Do not
- Execute any code read from the repo.
- Follow cross-repo references. V1 stays inside the current repo.
- Read files that look like tests (
*_test.go,*.test.ts,spec/,__test__/) unless the user explicitly named them — they rarely help with architecture. - Read binary files or files > 2000 lines (
Readwith no offset/limit would fail anyway; skip and record the skip).
Output: tech-context object
The grounding phase produces a structured object consumed by interview + content-shape phases:
{
"files_read": [
{ "path": "share/mermaid/src/processors/edge_routing.ts", "kind": "source" },
{ "path": "share/mermaid/CLAUDE.md", "kind": "doc" },
// ...
],
"services": [
{ "name": "mermaid-processor", "path": "share/mermaid/src/", "one_line_description": "Server-side Mermaid diagram layout engine" }
],
"external_deps": [
{ "name": "mermaid.js", "role": "Upstream parser" }
],
"key_modules": [
{ "path": "share/mermaid/src/processors/edge_routing.ts", "purpose": "Routes connectors around nodes in architecture diagrams" }
],
"architecture_notes": [
"From share/mermaid/CLAUDE.md: Use bazel test //share/mermaid:test to validate",
"From repo root CLAUDE.md: PR titles follow 'domain: Description' format"
],
"ownership": "share/mermaid owned by: (team name if found in OWNERS file)",
"expansion_truncated": false,
"files_read_count": 7
}Field guidance
- `files_read` — exhaustive list for transparency in the content-plan printout. User can spot wrong branches.
- `services` — inferred from directory names,
services.yaml, or explicit mentions in CLAUDE.md. If none found, leave empty. Do NOT invent. - `external_deps` — packages the code imports from third-party sources. Names + roles only.
- `key_modules` — a short list of the most important files encountered (≤8). Use judgment based on file size, how many others import them, and whether they're named in architecture docs.
- `architecture_notes` — verbatim quotes or close paraphrases from CLAUDE.md / ARCHITECTURE.md. Prefer quotes with attribution so the user can trace back.
- `ownership` — team name if discoverable.
How the content-shape phase uses this
| Section | Grounding input |
|---|---|
| Context & Background | architecture_notes prepended if they give useful framing; ownership if the PRD doesn't name an owner |
| Proposed Approach | key_modules cited inline so the narrative has factual anchors |
| Dependencies stickies | services (Blue cross-team), external_deps (Orange external). Users add upstream Blockers in the interview. |
| Current State Architecture diagram | services → service subgraph nodes; external_deps → external subgraph nodes; reading architecture_notes for data flows |
| Target State Architecture diagram | Current State plus additions from the PRD's Proposed Approach |
Safety
- Every file path that was read is printed back to the user in phase D's content plan. User approves before writes.
- If the user's entry point is outside the repo root, refuse and ask for an in-repo path.
- The skill never writes to or edits any file it reads as part of grounding.
Layout
Canvas geometry, sizing, positioning.
Constants
const LEFT_COL_X = 0;
const LEFT_COL_W = 800;
const GUTTER_X = 32;
const RIGHT_COL_X = LEFT_COL_X + LEFT_COL_W + GUTTER_X; // 832
const RIGHT_COL_W_MIN = 1200;
const GUTTER_Y = 32;
const SECTION_PAD = 32; // inner padding, all four sides
const METADATA_STRIP_H = 120;
const SECTION_TOP_Y = METADATA_STRIP_H + GUTTER_Y; // 152Canvas map
+--------------------------------------------------------+
| (0, 0) |
| [ Board metadata strip — full width, ~120px tall ] |
| |
| x=0 x=832 |
| +-----------+ +---------------------------+ |
| | LEFT COL | | RIGHT COL | |
| | 800 wide | | width driven by diagrams | |
| | [sec 1] | | [Current State] | |
| | ↓32 | | ↓32 | |
| | [sec 2] | | [Target State] | |
| | ↓32 | | ↓32 | |
| | ... | | [Key Flow 1] | |
| +-----------+ +---------------------------+ |
+--------------------------------------------------------+Left column stacking
curY_left = SECTION_TOP_Y;
for (const section of leftSectionsInOrder) {
section.x = LEFT_COL_X;
section.y = curY_left;
// After fill, section.resizeWithoutConstraints(LEFT_COL_W, section_h).
curY_left += section.height + GUTTER_Y;
}Right column stacking
curY_right = SECTION_TOP_Y;
for (const section of rightSectionsInOrder) {
section.x = RIGHT_COL_X;
section.y = curY_right;
// After fill, section.resizeWithoutConstraints(max(RIGHT_COL_W_MIN, diag.width + 64), h).
curY_right += section.height + GUTTER_Y;
}Right column x is fixed at 832 regardless of section width. Wider sections extend further right.
Inner padding rule
Every top-level section: first child's x = 32, y = 32. Last child's bottom edge + 32 = section height.
Sub-column positioning (for 3- or 4-column layouts inside a section)
For a 3-column layout inside an 800-wide section:
- Inner width =
800 - 2 * 32 = 736 - With 2 gutters of 32: each column =
(736 - 64) / 3 = 224wide - Column x positions:
32,288,544
For a 4-column layout:
- With 3 gutters of 32: each column =
(736 - 96) / 4 = 160wide - Column x positions:
32,224,416,608
See blocks/multi-column-text.md for formulas at runtime.
Section hug sizing
After appending children, compute:
const maxChildBottom = Math.max(...section.children.map(c => c.y + c.height));
section.resizeWithoutConstraints(LEFT_COL_W, maxChildBottom + SECTION_PAD);Sections do NOT auto-grow — always explicit resizeWithoutConstraints.
Placeholder lifecycle
- Skeleton pass:
section.placeholder = true. - End of fill pass for that section:
section.placeholder = false. - NEVER leave a section with
placeholder = trueat end-of-run.
Re-stack pass
After all left-column fills, a re-stack pass fixes cumulative Y based on actual post-resize heights:
let y = SECTION_TOP_Y;
for (const id of leftColumnSectionIdsInOrder) {
const sec = await figma.getNodeByIdAsync(id);
sec.y = y;
sec.x = 0;
y += sec.height + GUTTER_Y;
}Run this once between the left-column fill pass and the diagram pass.
Palette
STRICT DEFAULT — DO NOT DEVIATE WITHOUT USER OVERRIDE.
>
Project plan sections use a pale palette that visually pairs with the architecture-diagram subgraph wrappers. The FigJam SECTION palette (#CDF4D3,#C2E5FF,#DCCCFF,#FFE0C2, etc.) is too saturated for project-plan boards — it clashes with the diagram colors thatgenerate_diagramproduces. Use theARCH_PALEpalette below instead.
>
Two of these colors (#EBFFEEfor green,#F8F5FFfor violet) are pulled directly fromshare/mermaid/src/mermaid_v2/diagrams/processors/architecture/constants.ts— they are the section-wrapper fills the architecture layout uses forclientandservicesubgraphs. The rest are observed from a canonical reference board.
All palette colors use `hex/255` notation — e.g. {r: 0xEB/255, g: 0xFF/255, b: 0xEE/255}. Rounded decimals render as "Custom" in FigJam.
Helper:
const h = (r, g, b) => ({ r: r/255, g: g/255, b: b/255 });
const CHARCOAL = h(0x1E, 0x1E, 0x1E);
const WHITE = h(0xFF, 0xFF, 0xFF);ARCH_PALE — section background palette (THE canonical palette)
const ARCH_PALE = {
white: h(0xFF, 0xFF, 0xFF), // gateway, external, diagram sections
green: h(0xEB, 0xFF, 0xEE), // = architecture client section wrapper
violet: h(0xF8, 0xF5, 0xFF), // = architecture service section wrapper
blueLite: h(0xF5, 0xFB, 0xFF), // very pale blue (pairs with diagram datastore #BDE3FF)
blue: h(0xDB, 0xF0, 0xFF), // pale blue, slightly more saturated
orange: h(0xFF, 0xF7, 0xF0), // pale peach
teal: h(0xF1, 0xFE, 0xFD), // very pale teal
yellow: h(0xFF, 0xFB, 0xF0), // very pale yellow
pink: h(0xFF, 0xEE, 0xF8), // derived pale pink
red: h(0xFF, 0xEE, 0xE8), // derived pale red/coral
};Architecture diagram node fills (DO NOT MODIFY)
These come from CATEGORY_DEFAULT_STYLES in the architecture constants. generate_diagram applies them automatically — never override.
const ARCH_NODE_FILLS = {
client: h(0xAF, 0xF4, 0xC6), // mint green (rounded rectangle)
gateway: h(0xFF, 0xFF, 0xFF), // white (square; diamond if labeled "Load Balancer" / "ALB" / "LB")
service: h(0xE4, 0xCC, 0xFF), // light purple (square)
datastore: h(0xBD, 0xE3, 0xFF), // light blue (cylinder / ENG_DATABASE)
external: h(0xFF, 0xFF, 0xFF), // white (PREDEFINED_PROCESS / 3D-stacked)
async: h(0xBD, 0xE3, 0xFF), // light blue (ENG_QUEUE / stadium shape)
};Architecture subgraph section wrappers (only client and service get wrappers — gateway/datastore/external/async are bare shapes):
const ARCH_SECTION_WRAPPERS = {
client: h(0xEB, 0xFF, 0xEE), // = ARCH_PALE.green
service: h(0xF8, 0xF5, 0xFF), // = ARCH_PALE.violet
};Sticky palette
For sticky-column blocks (success metrics, risks, open questions). Stickies use a brighter palette than sections:
const STICKY = {
white: h(0xFF, 0xFF, 0xFF),
gray: h(0xE6, 0xE6, 0xE6),
green: h(0xB3, 0xEF, 0xBD),
teal: h(0xB3, 0xF4, 0xEF),
blue: h(0xA8, 0xDA, 0xFF),
violet: h(0xD3, 0xBD, 0xFF),
pink: h(0xFF, 0xA8, 0xDB),
red: h(0xFF, 0xB8, 0xA8),
orange: h(0xFF, 0xD3, 0xA8),
yellow: h(0xFF, 0xE2, 0x99),
};Table header palette — STRICT: light fill + dark text + Bold font
The reference board uses a TWO-TONE per-section palette: section background = ARCH_PALE (very pale), table header = matching FigJam SECTION palette (mid-tone, same hue), table cell text = `#1E1E1E` charcoal. Do NOT use dark fills with white text — that pattern is for FigJam's standalone tables, not for project-plan boards. The pale-on-pale two-tone is what makes the board feel cohesive.
const TABLE_HEADER = {
// Header row uses the FigJam SECTION palette — same hue as parent section, slightly more saturated
lightGray: { fill: h(0xD9, 0xD9, 0xD9), text: CHARCOAL },
lightGreen: { fill: h(0xCD, 0xF4, 0xD3), text: CHARCOAL },
lightTeal: { fill: h(0xC6, 0xFA, 0xF6), text: CHARCOAL },
lightBlue: { fill: h(0xC2, 0xE5, 0xFF), text: CHARCOAL },
lightViolet: { fill: h(0xDC, 0xCC, 0xFF), text: CHARCOAL },
lightPink: { fill: h(0xFF, 0xC2, 0xEC), text: CHARCOAL },
lightRed: { fill: h(0xFF, 0xCD, 0xC2), text: CHARCOAL },
lightOrange: { fill: h(0xFF, 0xE0, 0xC2), text: CHARCOAL },
lightYellow: { fill: h(0xFF, 0xEC, 0xBD), text: CHARCOAL },
};Both header AND body cells use `Inter Bold` (NOT Medium). Header text stays dark; body cell fill stays white (default).
Section → default ARCH_PALE color (slug-based mapping)
const SECTION_COLOR_BY_SLUG = {
motivation: ARCH_PALE.violet, // ideation/intro — pairs with service
context: ARCH_PALE.white,
goals: ARCH_PALE.green, // north star — pairs with client
approach: ARCH_PALE.green,
alternatives: ARCH_PALE.yellow, // caution / "considered"
designDecisions: ARCH_PALE.blue, // structured decisions — pairs with datastore
dependencies: ARCH_PALE.orange, // warnings / external dependencies
implementation: ARCH_PALE.violet, // active work — pairs with service
milestones: ARCH_PALE.blue, // time/sequencing
rollout: ARCH_PALE.orange, // launches / staged
risks: ARCH_PALE.pink, // hazards
diagram: ARCH_PALE.white, // right-column diagram sections
};Section → matching table header (STRICT mapping)
When a section contains a table, the table header uses the FigJam SECTION palette color matching the parent section's hue.
const TABLE_HEADER_BY_SECTION = {
motivation: TABLE_HEADER.lightViolet,
context: TABLE_HEADER.lightGray,
goals: TABLE_HEADER.lightGreen,
approach: TABLE_HEADER.lightGreen,
alternatives: TABLE_HEADER.lightYellow,
designDecisions: TABLE_HEADER.lightBlue,
dependencies: TABLE_HEADER.lightOrange,
implementation: TABLE_HEADER.lightViolet,
milestones: TABLE_HEADER.lightBlue,
rollout: TABLE_HEADER.lightOrange,
risks: TABLE_HEADER.lightPink,
};Two-tone effect: section bg = ARCH_PALE.X (very pale X-hue), table header = TABLE_HEADER.lightX (mid-tone X-hue), body cells white. This creates a coherent layered look across each section.
Text colors
Body text inside any pale section is CHARCOAL (#1E1E1E). White text on dark table headers and saturated stickies. Never use a colored text fill.
What NOT to use (and why)
| Wrong palette | Why not |
|---|---|
#CDF4D3 (FigJam lightGreen) | Too saturated. Mismatches diagram client wrapper #EBFFEE. |
#C2E5FF (FigJam lightBlue) | Too saturated. Visual jump next to diagram subgraphs. |
#DCCCFF (FigJam lightViolet) | Too saturated. Mismatches diagram service wrapper #F8F5FF. |
#FFE0C2 (FigJam lightOrange) | Too saturated. Use #FFF7F0 instead. |
#FFC2EC (FigJam lightPink) | Too saturated. Use #FFEEF8. |
If a user explicitly asks for the saturated FigJam palette, override per-section. Otherwise, ARCH_PALE is mandatory.
Plugin API Traps
The FigJam Plugin API accessed via use_figma has several dangerous mismatches between the top-level figma-use/SKILL.md rules and the per-node reference docs. Read this before writing any use_figma script for this skill.
Trap 1 — return vs figma.closePlugin()
| File | Says |
|---|---|
figma-use/SKILL.md rule #1 | Use `return` to send data back. Do NOT call figma.closePlugin(). Do NOT wrap in async IIFE. |
create-section.md Key Points | "Always wrap code in an async IIFE" and "Always call figma.closePlugin()" |
create-sticky.md Key Points | Same wrong guidance |
create-text.md Key Points | Same wrong guidance |
create-connector.md Key Points | Same wrong guidance |
create-table.md Key Points | Same wrong guidance |
create-code-block.md | Correct (uses return) |
Rule: figma-use/SKILL.md wins. When copying examples from the per-node docs, strip the IIFE wrapper and the closePlugin() call. Use return { createdNodeIds: [...], mutatedNodeIds: [...] }.
Trap 2 — Atomic error semantics
A failing use_figma script makes no changes to the file. Do not retry blindly: read the error, fix the script, then retry. Because the file is untouched, there are no partial nodes to clean up.
Trap 3 — resizeWithoutConstraints for sections
Sections do NOT support resize(). Use section.resizeWithoutConstraints(w, h). width and height are read-only otherwise.
Trap 4 — Coordinates are reparented by appendChild
When you call parent.appendChild(node), the node's x/y becomes relative to the parent's coordinate space. For sections, (0, 0) is the section's top-left. Call `appendChild` FIRST, then set `x`/`y`. Setting position before appending leaves the node in a confusing coordinate frame.
Trap 5 — Two-pass sticky layout
Sticky widths are fixed (240 or 416) but heights auto-grow when text overflows. Assuming a uniform 240×240 will cause overlaps.
// Pass 1: create all stickies and set text/color
const stickies = [];
for (const label of labels) {
const s = figma.createSticky();
await figma.loadFontAsync(s.text.fontName);
s.text.characters = label;
stickies.push(s);
}
// Pass 2: now read actual .width / .height and position
for (const s of stickies) {
// use s.width, s.height here — not 240×240
}Trap 6 — hex/255 notation for palette colors
FigJam palette matching is exact. Rounded decimals make FigJam treat the color as "Custom" instead of a palette color.
// WRONG — will render as Custom
{ r: 0.76, g: 0.89, b: 1.00 }
// CORRECT — matches the Light blue palette swatch
{ r: 0xC2/255, g: 0xE5/255, b: 0xFF/255 }Use a helper:
const h = (r, g, b) => ({ r: r / 255, g: g / 255, b: b / 255 });Trap 7 — Font must be loaded before any text operation
const text = figma.createText();
await figma.loadFontAsync(text.fontName); // REQUIRED before `characters`, `fontSize`, etc.
text.characters = "Hello";Not required for color-only changes (text.fills = [...]).
Trap 8 — Connector text.fontName is invalid by default
A newly-created connector's text.fontName cannot be loaded directly. You MUST explicitly set text.fontName to a known loaded font, then set text.characters.
const font = { family: 'Inter', style: 'Medium' };
await figma.loadFontAsync(font);
const c = figma.createConnector();
c.connectorStart = { endpointNodeId: a.id, magnet: 'AUTO' };
c.connectorEnd = { endpointNodeId: b.id, magnet: 'AUTO' };
c.text.fontName = font;
c.text.characters = 'depends on';Existing connectors with labels DO have valid text.fontName — load directly.
Also: visible label is `connector.text.characters`, NOT `connector.name`. name only changes the layers panel.
Trap 9 — Position new top-level nodes away from (0, 0)
Nodes appended directly to a page default to (0, 0). If content already exists there, the new node overlaps it. Scan figma.currentPage.children and pick a clear spot.
Nodes nested inside frames/sections don't have this problem — the parent positions them.
Trap 10 — layoutSizingHorizontal/Vertical = 'FILL'
Set this AFTER parent.appendChild(child). Setting before append throws. Same for 'HUG' on non-auto-layout nodes.
Trap 11 — figma.currentPage = page does NOT work
Use await figma.setCurrentPageAsync(page). The sync setter throws "Setting figma.currentPage is not supported" in use_figma. figma.currentPage also resets to the first page at the start of every use_figma call.
Trap 12 — figma.notify() throws
It's not implemented under use_figma. Never call it.
Trap 13 — Return ALL created/mutated node IDs
console.log() output is NOT returned. The agent only sees the return value. Collect every affected node ID:
return {
createdNodeIds: [...],
mutatedNodeIds: [...],
// optional: counts, status, screenshot metadata
};Downstream calls need these IDs to reference what was created.
Trap 14 — Colors are 0–1, not 0–255
// WRONG
{ r: 194, g: 229, b: 255 }
// CORRECT
{ r: 194/255, g: 229/255, b: 255/255 }Color objects are { r, g, b } only — no a field. Opacity goes at the paint level: { type: 'SOLID', color: {...}, opacity: 0.5 }.
Trap 15 — fills and strokes are read-only arrays
Clone, modify, reassign. Don't mutate in place.
// WRONG
node.fills[0].opacity = 0.5;
// CORRECT
const fills = JSON.parse(JSON.stringify(node.fills));
fills[0].opacity = 0.5;
node.fills = fills;Trap 16 — Batch size
Keep each use_figma call to ≤10 logical operations (a "logical operation" ≈ create + configure + parent one node). Split large builds across multiple calls. Validate between.
Trap 17 — Section resize after adding children
Sections DO NOT auto-grow. After appendChild, measure children and call resizeWithoutConstraints with (maxChildRight + 32, maxChildBottom + 32) to fit with 32px padding. Exception: sections that are part of a grid layout must keep uniform dimensions — don't hug individually.
Trap 18 — Sticky palette ≠ section palette ≠ text palette
Three different palettes, all named from the same color names:
- Sticky palette (from
create-sticky.md): 10 brighter colors — White, Gray#E6E6E6, Green#B3EFBD, Teal#B3F4EF, Blue#A8DAFF, Violet#D3BDFF, Pink#FFA8DB, Red#FFB8A8, Orange#FFD3A8, Yellow#FFE299. - Section palette (from
create-section.md): 10 lighter colors — White, Light gray#D9D9D9, Light green#CDF4D3, Light teal#C6FAF6, Light blue#C2E5FF, Light violet#DCCCFF, Light pink#FFC2EC, Light red#FFCDC2, Light orange#FFE0C2, Light yellow#FFECBD. - Text palette (from
create-text.md): 22 colors including Charcoal#1E1E1E(default; use this for body text unless specified otherwise).
Don't cross-pick — a "Blue" sticky is #A8DAFF, a "Blue" section is #C2E5FF, they are NOT the same.
Trap 19 — Table sizing
Tables are FigJam-only (figma.createTable(rows, cols)). width and height are read-only. Use resizeRow(i, h) and resizeColumn(j, w). Rows and columns cannot go below their minimum. cellAt(r, c) for cell access; load the cell's font before setting text.characters or text.fills.
Trap 20 — The code block node is first-class, not a shape
figma.createCodeBlock(). Set cb.code (not cb.text.characters) and cb.codeLanguage from a fixed enum (TYPESCRIPT, JAVASCRIPT, PYTHON, GO, RUST, RUBY, CSS, HTML, JSON, GRAPHQL, SQL, SWIFT, KOTLIN, CPP, BASH, PLAINTEXT). No font loading required. FigJam-only.
Section catalog
The ~10 candidate sections the skill proposes in Step 2. Each entry defines: what the section is for, when to suggest it, default block shape, default section palette color, default table header color (if it has a table), and gap questions for Step 3.
Use these as defaults — the user can pick alternative shapes during Step 4.
---
motivation — Motivation
Title: Motivation When to suggest: PRD has a non-trivial problem statement or background narrative (>= 3 sentences, or a named recent incident / stakeholder ask). Default shape: intro-callout.md + table.md for "Resources". Palette: lightViolet section bg; violet table header. Contents (default):
- Intro callout — one-sentence "why this exists" pulled from the PRD.
- H3 "Resources" subheader.
- Table of external resources: columns
Type | Link | Description.
Gap questions:
- Any resources beyond the PRD to list (design doc, Coda doc, Slack thread)?
---
context — Context & Background
Title: Context & Background When to suggest: PRD has a clear "background" or "current situation" paragraph not suitable as an intro callout. Default shape: body paragraph only (text-primitives.md). Optional H3 + bulleted list for related resources. Palette: lightGray section bg. Contents (default):
- Body text — 3–6 sentence paragraph synthesizing problem + motivation.
- (Optional) H3 "Related Resources" + bulleted list of links.
Gap questions:
- Is there a recent incident or stakeholder ask behind this project? (free text)
---
goals — Goals, Non-Goals & Success Metrics
Title: Goals, Non-Goals & Success Metrics When to suggest: Always. Even a PRD without explicit metrics can list goals + non-goals. Default shape: 3-column multi-column-text.md: Goals / Non-Goals / Success Metrics. Optionally: a table.md with columns Goal | Description instead of the bulleted list, if user prefers structured. Palette: lightGreen section bg; green table header. Contents (default):
- Col 1 (Goals): H3 "Goals" + bulleted list from PRD.
- Col 2 (Non-Goals): H3 "Non-Goals" + bulleted list ("none" allowed).
- Col 3 (Success Metrics): H3 "Success Metrics" + sticky-column.md of yellow stickies, one metric per sticky.
Gap questions:
- Any non-goals (things explicitly not shipping in v1)? (free text)
- Success metrics, each as "metric: target" (e.g. "p95 < 200ms"). (free text)
---
approach — Proposed Approach
Title: Proposed Approach When to suggest: PRD has a chosen approach or solution outline. Default shape: body paragraph + optional H3 "Key Design Decisions" + bulleted list. Palette: lightGreen section bg. Gap questions:
- One-sentence summary of chosen approach. (free text)
- 2–4 key design decisions to highlight. (free text)
---
designDecisions — Design Decisions (with alternatives)
Title: Design Decisions: 1, 2, … N When to suggest: The user identified >= 2 design decisions in the Proposed Approach, each with alternatives or tradeoffs. This section uses nested sections + multi-column text. Default shape: nested-section.md parent → one child per decision. Each child: H3 + body + table.md for Tradeoffs + multi-column-text.md for N options. Palette: lightBlue parent section; lightBlue child sections (matching); blue table header. Gap questions:
- For each design decision: name + 2–4 alternatives + tradeoff axes (e.g. "Latency", "Complexity", "Cost"). (free text per decision)
---
alternatives — Alternatives Considered
Title: Alternatives Considered When to suggest: The PRD + interview mention >= 1 serious alternative that was rejected, AND designDecisions is not selected (they overlap). Skip if the alternatives live inside designDecisions. Default shape: H3 per alternative + body paragraph of one-line rejection reason. Palette: lightYellow section bg. Gap questions:
- For each alternative: name + one-line rejection reason. (free text)
---
dependencies — Dependencies
Title: Dependencies When to suggest: The PRD or codebase grounding identified cross-team services, external vendors, or blockers. Default shape: table.md with columns Type | Dependency | Notes. Alternative shape: sticky-column.md (orange stickies), single column. Palette: lightOrange section bg; orange table header. Gap questions:
- External vendors the project depends on? (free text)
- Cross-team services? (free text; pre-filled from tech-context if available)
- Known blockers? (free text)
---
implementation — Implementation Plan
Title: Implementation Plan When to suggest: Always (even a rough phasing is useful). Default shape: table.md or H3-per-phase + bulleted list. Columns for the table: # | Phase | Timeline | Sub-tasks. Palette: lightViolet section bg; violet table header. Gap questions:
- Phases (name + timeline + sub-tasks)? (free text; suggest 3–5 phases with 3–8 sub-tasks each)
---
milestones — Milestones
Title: Milestones When to suggest: The user has explicit dates or week-numbered deliverables, OR the implementation phases have clear dates attached. Default shape: table.md with columns # | Phase | Timeline | Description. Numbered first column. Palette: lightBlue section bg; blue table header. Gap questions:
- List milestones with # / name / week or date / one-line description.
---
rollout — Rollout & Validation
Title: Rollout & Validation When to suggest: The PRD or interview mentions a rollout strategy (feature flag / canary / big-bang). Default shape: body paragraph (rollout strategy summary) + table.md with columns Stage | Activities | Metric to Watch | Gate. Palette: lightOrange section bg; orange table header. Gap questions:
- Rollout strategy (feature flag / canary / blue-green / big-bang / custom).
- Stages — each with activity, metric, gate.
- Test strategy (unit / integration / load / manual QA).
---
risks — Risks & Open Questions
Title: Risks & Open Questions When to suggest: Always (every real project has at least one of each). Default shape: 2-column multi-column-text.md with H3 "Risks" (one column) and H3 "Open Questions" (other column), each as a sticky-column.md. Alternative: bulleted list if user prefers plain text. Palette: lightPink section bg. Stickies: Risks → pink or red; Open questions → blue. Gap questions:
- Top risks (things that could block or break this). (free text)
- Open questions (decisions still pending). (free text)
---
currentState — Current State Architecture (diagram, right column)
Title: Current State Architecture When to suggest: The project touches existing services (tech-context has >= 1 service) and isn't greenfield. Default shape: diagram-section.md. Palette: white section bg. Gap questions: none — composed from tech-context by generate_diagram.
---
targetState — Target State Architecture (diagram, right column)
Title: Target State Architecture When to suggest: Always (project's endpoint, even greenfield has a target state). Default shape: diagram-section.md. Palette: white section bg. Gap questions: what's new vs. current state (fed into Mermaid composition).
---
keyFlow — Key Flow N (diagram, right column; 0–N instances)
Title: Key Flow: <flow name> When to suggest: The user explicitly names user journeys or system flows worth diagramming (e.g. "Sync trigger flow"). Default shape: diagram-section.md. Palette: white section bg. Gap questions: for each flow — name + short description + trigger + actors.
---
Suggestion rules (Step 2 use)
1. Always-suggest: goals, risks, targetState. 2. Suggest if content exists:
motivationif PRD problem statement ≥ 3 sentences OR has resources beyond itself.contextif PRD has a separate background section (not already in motivation).approachif PRD has a chosen solution.designDecisionsif ≥ 2 decisions with alternatives.alternativesif alternatives not already insidedesignDecisions.dependenciesif tech-contextservicesorexternal_depsis non-empty OR PRD lists any.implementationif PRD has any phasing, or if the user mentions timeline.milestonesif PRD has explicit dates or week-numbered deliverables.rolloutif PRD mentions flags, canary, validation, QA strategy.currentStateif tech-context has ≥ 1 existing service.keyFlow(N) if user explicitly names flows.
3. Skip:
- Anything with no content (padding sections are worse than missing sections).
alternativeswhendesignDecisionswas selected — they'd duplicate.
When suggesting a section in the Step 2 cards, include which PRD facts or tech-context items justify it (e.g. "suggesting dependencies because tech-context has services: a, b, c").
Related skills
FAQ
What does generate-project-plan do?
generate-project-plan builds confirmed FigJam project plan boards from PRDs.
When should I use generate-project-plan?
User wants FigJam project plan, /generate-project-plan, or PRD board.
Is this skill safe to install?
Review the Security Audits panel on this page before installing in production.