The front door for Figma work. The official Figma plugin ships eleven skills that own the Plugin API surface; the community ships fifty more for tokens, components, a11y and docs. This skill does not replicate any of them. It does three things they don't:
- Routes — says which skill or tool to load for a given job (§1).
- Composes — capture → curate → arrange workflows for moodboards, reference boards and any "put these images in Figma, beautifully" request (§3–§5).
- Guards — the canvas craft rules that sit above the API rules: aspect ratio, z-order, overlap budgets, verification loops, page hygiene (§6).
Always load
figma-usebefore anyuse_figmacall. It is the source of truth for the Plugin API and its gotchas. This skill assumes it is loaded and cites its references rather than restating them.
1. Router — which skill for which job
| The job | Load | Tool it wraps |
|---|---|---|
| Any script that mutates or reads the canvas | figma-use (mandatory prerequisite) |
use_figma |
| Design → code, implement a screen | the design-to-code guidance served as MCP resource skill://figma/figma-design-to-code/SKILL.md (not a plugin skill) |
get_design_context, get_screenshot |
| SwiftUI ↔ Figma, either direction | figma-swiftui |
get_design_context, use_figma |
| Build a page/screen from app code | figma-generate-design + figma-use |
use_figma, generate_figma_design |
| Tokens, variables, component library | figma-generate-library + figma-use |
use_figma, search_design_system |
| Map components to code | figma-code-connect |
Code Connect tools |
| New blank file | figma-create-new-file |
create_new_file |
| Mermaid-class diagram in FigJam | figma-generate-diagram |
generate_diagram |
| FigJam board content | figma-use-figjam + figma-use |
use_figma |
| Slides deck | figma-use-slides + figma-use |
use_figma |
| Motion / animation | figma-use-motion, figma-implement-motion |
use_figma, get_motion_context |
| Images or SVGs into a file | this skill §4 | upload_assets |
| Moodboard, reference board, composed arrangement | this skill §3–§5 | upload_assets + use_figma |
| Live-site captures as source imagery | shotcraft → this skill §4 |
— |
| Design-token export/import, a11y audits, variable CRUD | community skills — see references/skill-map.md | varies |
Two routing rules the tables can't express:
- A read-only inspection comes first, always. Pages, existing frames, fonts, naming
conventions.
figma-use§9 has the scripts. Never create before you have looked. get_metadataneeds editor access, not viewer. "You don't have edit access" on a file you can open in the browser means the share is view-only. Ask for editor, or check you are on the right account (§2).
2. Multi-account routing
One Figma OAuth token binds to exactly one Figma account. If the user belongs to more than one org (agency + client, personal + work), the correct setup is one MCP server per account — typically the plugin server for one and a claude.ai connector for the other. They expose separate tool namespaces; pick the account per call by prefix.
- Confirm identity with
whoamion each server before assuming which file it can see. - Do not consolidate to one server: every account switch would become an interactive OAuth re-auth, impossible from a non-interactive session.
- The durable fact is mechanism → account (plugin = X, connector = Y). Connector UUIDs change on re-add — never key notes on them.
- A View-only team seat fails the same way as a view-only share.
whoamilists seats.
3. Composition workflow — capture → curate → compose
The phases, each with an exit gate. Post a short checklist before each phase and a
summary after (the phase contract in figma-generate-library §1 is the model —
visible progress, decisions surfaced, never silently defaulting).
| Phase | Output | Exit gate |
|---|---|---|
| 0 Brief | Sources list, art direction (2–3 sentences), palette/vocabulary if any, target file + page | User has confirmed the source list; blockers named (attached-not-sent images, login-walled sites) |
| 1 Capture | Screenshots / section crops / user-supplied images on disk, at true pixel dimensions | Every file exists; dimensions read from the file header, not assumed |
| 2 Curate | Shortlist with one line per image saying what it contributes | Every image has been looked at by the agent. Unvetted images never enter a composition |
| 3 Upload | Nodes in the target file, named after their source files | Node IDs returned and recorded (§7 ledger) |
| 4 Arrange | Images resized to true aspect ratio and placed per a chosen pattern (§5) | Screenshot reviewed; collisions and defects fixed before adding chrome |
| 5 Dress | Typography, hairlines, marks, labels — only what the brief's vocabulary calls for | Second screenshot; nothing overlaps text; single accent rule honoured if one exists |
| 6 Hand back | Rendered PNG sent to the user; page name; what was left open | The user sees the render, not just a description of it |
Hard gates:
- No
use_figmamutation before Phase 0's source list is confirmed. - No image placed before it has been opened and looked at (Phase 2). The gate exists because the mistakes that slip through are obvious to eyes and invisible to exit codes.
- No chrome (labels, rules, marks) before the bare arrangement screenshot is clean.
- Build on a new page; never restructure the user's existing page in place. Deleting the old pages is a separate, explicit request (§6).
4. Capture → canvas
Sources. Three kinds, in rising order of value for a moodboard:
- Full-page screenshots — good for scroll strips, bad for boards (a 16,000px strip compressed to board width is an unreadable column; use the viewport shot instead).
- Section crops — the useful unit.
shotcraft'sprobe-sections.mjsfinds them structurally;capture.mjswithelements[]/scrollTotakes targeted ones. - Designed comps the user supplies — usually the strongest material; these are the aesthetic executed, not referenced.
Stage first. scripts/stage-assets.mjs takes a folder and/or a shortlist, copies
each image to a staging directory under a meaningful slug (07-ref-collected-system.png,
never IMG_0997.PNG — the filename becomes the Figma layer name on upload), reads
true pixel dimensions from the file header, and emits the planner's input JSON.
It exits 10 until every image has a subject (--names) and an arm (--arms): the
grouping is a human decision and the script refuses to guess it.
node scripts/stage-assets.mjs --list shortlist.txt --dir ./refs --out ./staged \
--names names.json --arms arms.json --json > board.json
Upload. upload_assets with count: N, then POST each staged file as
multipart/form-data with a file field. Record the returned placedOnNodeId per
file into board.json's id fields (§7 ledger).
The 400×300 trap. Uploaded images land as 400×300 frames with scaleMode: FILL,
which crops. The frame tells you nothing about the image; only the header does.
resize() every frame to the planner's w×h before placing it.
Uploads land on whichever page is current for the upload tool — not necessarily
the page your last script switched to. Find them by ID and appendChild them where
they belong.
5. Arrangement patterns
Three patterns, in the order a session usually discovers them. Details, coordinates and the reasoning behind each choice live in references/moodboard-composition.md.
| Pattern | When | Character |
|---|---|---|
| Column grid | Reference sheet, equal-weight items, "make it aligned" | 3 columns + 2-col spans; every rotation 0; captions left-aligned to column |
| Rigid plus | Groups have a meaning along each axis (medium ↑↓, temperature ←→) | Four arms from a centre element, ring sizes stepping ~0.72× outward, axes drawn as hairlines with terminal dots |
| Loose plus | "Organic", "clustered around the centre", "loosely a plus" | Same grouping, axes removed, inner ring overlapping the centre by ~40px and nudged off-axis, second ring tucked onto the inner ring's corners (≤ 250×80), wordmark floating in the eye |
Rules that held across all three:
- Rotation is opt-in. Zero degrees unless the user asks for angles; "organic" means offset and overlap, not tilt.
- Group by what images share — type, aesthetic, or colour — and let the axes mean something. A plus with arbitrary arms is just a cross.
- Sizes step toward the centre. Largest adjacent to the centre, ~0.72× per ring.
- Overlap has a budget. Inner ring onto centre ≤ 40px; neighbour corners ≤ 250×80. The planner reports every overlap and exits 10 when one exceeds budget.
- A centre element is allowed to be typographic. The brand at the centre of its influences reads better than a borrowed image there — but once images overlap it, strip its chrome (strokes, ticks, metadata) or it reads as a broken box.
- Vocabulary in the quadrants or corners, never in the cluster.
Plan coordinates with the script rather than by hand, and generate the placement script rather than typing it:
node scripts/plan-layout.mjs --input board.json --mode loose --jitter 24 --seed 7 --json > plan.json
node scripts/emit-placement.mjs --plan plan.json --phase backdrop # → use_figma
node scripts/emit-placement.mjs --plan plan.json --phase place --backdrop 45:2 --captions
plan-layout emits backdrop-relative {id, x, y, w, h} placements in z-order,
canvas size, and an overlap report (exit 10 over budget). --jitter adds seeded
irregularity for "organic" without losing reproducibility — same seed, same plan.
emit-placement turns the plan into the exact use_figma script, and refuses
any image whose plan entry is vetted: false — the look-at-it gate as data, not
memory. Full flow in references/place-and-verify.md.
6. Canvas craft rules (above the API)
Learned the expensive way; each one cost a round-trip this skill now saves.
- Hairline stroke on every dark image. A dark site on a dark ground disappears; a
1px
SOFT45–55% stroke,strokeAlign: OUTSIDE, defines the edge. Checkfillsbefore concluding an image "didn't render". - Z-order is append order. Anything that overlaps must be appended after what it
sits on. Send axis lines to the back with
insertChild(0, …). - Font style names are per-family, verify them. Inter uses
"Semi Bold"; Archivo uses"SemiBold".listAvailableFontsAsync()first, always. - Load fonts before any text property, including
textAlignHorizontalon an existing node. Scripts are atomic — a font error means nothing ran, so fix and retry safely (figma-usegotchas: canonical text-edit recipe). - Never filter nodes by size to find your marks.
width <= 38also matched the 9px footer squares and moved them 440px. Track IDs; filter by name or ID. - Deleting a label deletes only the text. Its leader line and dot stay. Remove the whole triplet, or rebuild all captions after a re-layout rather than nudging.
- Verify mechanically, then screenshot. Read the board back (the read-only script
in references/place-and-verify.md) and run
scripts/verify-board.mjs --plan plan.json --board readback.json. It catches what eyes don't: frames still at 400×300, drift, inverted z-order, budget breaches, missing strokes, rotation. Thenget_screenshoton the board node atmaxDimension1400–1800 for what programs can't judge: collisions of meaning, stranded chrome, whether it is any good. Node-level screenshots (contentsOnly) are for detail, not verification. - Renders go to the user.
curlthe screenshot URL todesign/exports/and send the file; a description of a board is not a board. - Page deletion is explicit and last. Switch
currentPageto the survivor first (you cannot remove the current page), clone-don't-move when building alternatives, and remind the user that version history holds the deleted pages.
7. State ledger for long builds
Context gets summarised mid-build. Keep a ledger on disk from Phase 3 onward:
{ "file": "<fileKey>", "page": "44:3", "backdrop": "45:2",
"images": { "07-ref-collected-system": "44:5" },
"chrome": { "captions": ["38:2","38:3"], "vocab": ["47:2"] },
"exports": ["design/exports/board-v3.png"] }
Re-read it at the start of every turn; reconstruct by name (page.query('FRAME[name^=07-]'))
if it is missing. Idempotency is by node name — never re-upload an image whose name
already exists on the page.
8. Decision forks — ask, don't default
Ask when two arrangements are both defensible and the brief doesn't decide; present each with its cost. Do not ask about things the source decides (an image's aspect ratio, whether a dark image needs a stroke). Rejected work is never built on — if the user says "not jaunty angles", every rotation goes to zero before the next screenshot, not just the new ones.
References
- references/moodboard-composition.md — the three patterns with coordinates, grouping logic, and what each round taught.
- references/capture-to-canvas.md — shotcraft → upload → true-AR placement, end to end, with the dimension reader.
- references/skill-map.md — the official + community Figma skill landscape and what this skill deliberately leaves to them.
- references/lessons.md — the session log this skill was distilled from, kept as evidence for the rules in §6.
- references/place-and-verify.md — the last mile:
fill in node IDs, emit the two
use_figmascripts, read the board back, verify. scripts/stage-assets.mjs— folder/shortlist → slug-named copies + true dimensions → planner JSON (vetted: falseby construction). Exits 10 until subjects and arms are supplied.scripts/plan-layout.mjs— deterministic layout planner (grid / plus / loose), seeded--jitter, overlap budget, captions andvettedpassed through.scripts/emit-placement.mjs— plan → exactuse_figmascripts (backdrop, place, optional captions). Refuses unvetted images.scripts/verify-board.mjs— board read-back vs plan: size, position, rotation, z-order, overlap budget, strokes. Exit 10 with findings.scripts/verify-freshness.mjs—--offline: every skill the router names exists in the local plugin cache;--live: the community index vsskill-map.md.assets/plus-layout.example.json— the real 14-image Agntik fixture.assets/light-board.example.json— a second real fixture (8 light-ground captures, smaller centre, slug IDs, captions) so defaults are validated on more than one board.
The pipeline, end to end: shotcraft (or a folder) → stage-assets → look at every
image, set vetted, caption, arm → plan-layout → emit-placement (backdrop)
→ upload_assets → fill IDs → emit-placement (place) → read back → verify-board
→ get_screenshot → dress → send the render.