About penpot-build-deck
penpot-build-deck is a skill for turning a brief into editable slide designs in Penpot. Its staged process builds one slide per call, reviews exported slides, and wires the finished boards into a playable flow.
Key features
- 01Plans a complete slide outline first
- 02Builds editable 1920×1080 slide boards
- 03Applies deck tokens and typography
- 04Reviews each slide through exports
- 05Checks layout variety and deck quality
- 06Wires slides into a playable flow
Use cases
- 01Creating an editable presentation from a brief
- 02Building slides in reviewed batches
- 03Preparing a deck for Penpot View mode and PDF export
MCP support
The skill uses Penpot MCP tools to create and edit pages, boards, layouts, text, tokens, media, and flows. It references the Penpot MCP tool surface for implementation and review.
Install
Copy the command and run it in a terminal.
npx skills add https:/ / github.com/ penpot/ penpot-ai-kit --skill penpot-build-deckAfter installing, type/penpot-build-deckin Claude Code to confirm it loaded.
Bundled files
References and scripts installed along with SKILL.md.
references/01-deck-brief-analysis.md02-deck-styles.md03-slide-archetypes.md04-typography-and-grid.md05-visuals-and-effects.md06-flow-and-playback.md07-critique-framework.md08-anti-rationalization.md09-error-recovery.mdscripts/applyDeckEffects.jsauditDeckQuality.jsbuildSlide.jscreateDeckPage.jssetupDeckSystem.jswireDeckFlow.jsDetails
From the project READMEpenpot-build-deck — brief to a playable slide deck
1. Title + How it works
penpot-build-deck turns a brief into a presentation that lives in Penpot as real, editable design:
one 1920×1080 Board per slide on a dedicated page, tokenized through a deck.* token set, wired
into a flow so the deck plays in Penpot's View mode (←/→, Shift+F fullscreen), and exportable to
PDF. Every mutation goes through execute_code; validate visually with export_shape (per slide,
never the whole page); read structure with penpotUtils.shapeStructure (full tool surface:
shared/penpot-mcp-tool-reference.md). Penpot has no "slides mode" — this skill is the deck
tooling: outline → deck system → style freeze → slides in batches → flow + quality gate.
2. The One Rule That Matters Most
Plan the whole outline, then build one slide per call. Never generate a deck in one
execute_code. Slides 1–3 are the style freeze: everything after them inherits their decisions.
Build the rest in batches of 3–5, export and look at every slide before its checkpoint, and refuse
the AI-slide tells (accent lines under titles, decorative bars, centered body text, text-only slides,
the same layout twice in a row).
3. Penpot MCP Tool Reference
Full surface: shared/penpot-mcp-tool-reference.md. Domain calls: penpot.createPage() /
openPage() (two-call page protocol); createBoard() + resize(1920, 1080); addFlexLayout() /
addGridLayout(); createText() + penpot.fonts.all (exact name) + font.applyToText();
applyToken; penpot.uploadMediaUrl(name, url); page.createFlow(name, board);
shape.addInteraction("click", { type: "navigate-to", destination, animation });
board.waitForLayoutUpdate(); export_shape(slideId).
4. Plugin API Essentials
Gotcha numbers refer to shared/plugin-api-gotchas.md.
- #15 page targeting — this skill's critical failure mode. Decks live on their own page, and a
board born on the wrong page cannot be moved (cross-page
appendChildis a silent no-op).scripts/createDeckPage.jsruns the two-call protocol: call A creates + opens the page, call B assertspenpot.currentPage.idmatches before anycreateBoard(). If B fails, stop and ask the user to open the page in the UI — never build on the wrong page. - #11 fill policy. The slide board is the surface:
fills = [], then binddeck.bg.<tone>. Every inner container (safe-area, columns, rows, cards that are pure grouping) staysfills = []; only real cards/panels binddeck.surface.*. - #13b exact fonts.
penpot.fonts.all.find(f => f.name === "Inter")—findByNamematches substrings and will hand you "Inter Tight" or "Roboto Mono". Re-assertfontSize/lineHeightafterapplyToText. - #14 edge decorations are absolute children positioned in page coordinates; boards clip at their edges — that clipping is the bleed.
- #16
clearFlipafter every slide (shared/visual-effects.md§7); the mirror is invisible toshapeStructureand unmissable in the export. - #17
await board.waitForLayoutUpdate()before reading geometry in the same call. - #18 image fills are async and fallible — placeholder by default, uploads only for user URLs, verified in the next call.
- #2 tokens apply asynchronously — bind in the build call, verify
shape.tokensin the audit. - Verify unfamiliar signatures with
penpot_api_infofirst (Page.createFlow,Shape.addInteraction,Board.backgroundBlur,GridLayout).
5. Token-Aware Brief Contract
- Context — audience, occasion, duration, venue/screen, language.
- Objective — the one message the audience must leave with, and the ask / CTA.
- Inputs — outline (one line per slide), facts and figures with sources, quotes with attribution, image URLs (or "placeholders"), existing tokens/components/fonts to reuse.
- Constraints — no fabricated numbers; fonts must exist in
penpot.fonts.all; every slide gets a visual; forbidden: accent lines under titles, decorative bars/stripes, centered body text, default blue, cream backgrounds, text-only slides;deck.*tokens only (gradients/shadows/glass are ledgered literals); with strict anti-slop on, the keys the direction step refused (shared/anti-slop.md§1, deck readings inpenpot-anti-slopreferences/04-deck-tells.md). - Acceptance Criteria — every slide a 1920×1080 board named
NN-archetype-slug; ≥ 1 visual per slide; no two consecutive identical archetypes and no archetype > 40 % of the deck; type floor 20 px, body ≥ 28 px; contrast ≥ 3:1 (text ≥ 24 px or ≥ 18.67 px bold) / 4.5:1 below; flowDeckwired click → next; deck-quality score ≥ 3 on all seven axes or the weak axes named; with strict anti-slop on, slop score ≤ 35 over the deck page.
Act as a senior presentation designer who makes a real scale difference, uses the full canvas, varies the anchor slide to slide, and never ships a text-only slide.
6. Mandatory Workflow
Visual self-review (mandatory): before every ✋ checkpoint, run the export → look → fix loop of
shared/visual-self-review.mdon each slide built since the last checkpoint (max 2 self-fix iterations per checkpoint), and present those same exports with any remaining defects named.
Phase 0 — Discovery + outline (read-only). high_level_overview; penpotUtils.tokenOverview();
penpot.fonts.all.map(f => f.name); penpotUtils.getPages(). Analyse the brief
(references/01-deck-brief-analysis.md): slide budget, the one message, outline table
n | archetype | title | key content | visual | tone, tonal arc, motif. Pick a deck style
(references/02-deck-styles.md) and run the variety pre-check (references/03-slide-archetypes.md).
Anti-slop opt-in: read the stored preference (penpot-anti-slop → scripts/antiSlopPrefs.js); if
unset and the brief doesn't answer it, ask the shared/anti-slop.md §0 question once (user's
language) and store it. When on, run the direction step (penpot-anti-slop
references/02-direction-playbook.md + 04-deck-tells.md) before picking the deck style, and add
its refused keys to the Constraints.
✋ Checkpoint: approve outline + style + tonal arc + slide count (+ direction block if strict).
Phase 1 — Deck system. scripts/createDeckPage.js (PHASE=create, then PHASE=verify in the next
call). scripts/setupDeckSystem.js: token set deck (aliasing existing brand tokens where they
exist), font resolution, optional master components. New tokens are proposed here, never invented
silently. ✋ Checkpoint: approve tokens + fonts.
Phase 2 — Style freeze. Build 01-cover-…, 02-agenda-… and the first content slide with
scripts/buildSlide.js (+ scripts/applyDeckEffects.js for the background device), one slide per
call; export each; self-review. ✋ Style-freeze checkpoint — any later style change reopens it.
Phase 3..N — Batches. 3–5 slides per batch, one slide per call, clearFlip in every call; after
the batch, export each slide and check the variety rule against the previous slide. Image URLs are
uploaded in applyDeckEffects.js and verified in the following call. ✋ Checkpoint per batch.
Phase N+1 — Wire, gate, critique. scripts/wireDeckFlow.js (order, flow Deck, click → next,
optional auto-advance, section flows). scripts/auditDeckQuality.js — a non-pass gate blocks
"done". Then the scored critique (references/07-critique-framework.md): seven axes + the monotony
check, any axis < 3 → targeted revision (max 2 passes) → Markdown + JSON per
shared/report-schemas/deck-quality-report.schema.json, mirrored to the ledger. Strict mode on: run
penpot-anti-slop diagnose (MODE = "deck") on the deck page and include the slop score. Give the PDF-export
and share-link steps (references/06-flow-and-playback.md). ✋ Final checkpoint.
7. Critical Rules
- One slide per
execute_codecall. The outline is planned whole; the canvas is built one slide at a time. - Every slide is a 1920×1080 Board named
NN-archetype-slug,showInViewMode = true,clipContent = true, placed on the deck page's contact grid. - Fill policy (#11): the slide board binds
deck.bg.<tone>; grouping containers are transparent. - Tokens first:
deck.*for colour, type sizes/families, spacing, radius. Gradients, glass fills and shadows cannot bind — every one is a ledgerexceptionsentry with the tokens it derives from. - ≥ 1 visual element per slide (shape, image/placeholder, stat numeral ≥ 140 px, chart placeholder, component instance). Text-only slides fail the gate.
- Variety rule: consecutive slides differ on ≥ 1 of archetype / anchor / tone; never the same archetype twice in a row; no archetype > 40 % of the deck.
- Type floor 20 px; body ≥ 28 px; titles 72–96 px (
references/04-typography-and-grid.md); ≤ 2 families, ≤ 3 weights; sentence case; body left-aligned. - Honest content: numbers only from the brief, each with a source line; quotes attributed; no lorem, no "Item 1".
- Images: labeled placeholders unless the user supplied URLs; uploads verified next call; failures reported, never swapped silently.
- Refuse the tells: accent lines under titles, decorative bars/stripes, centered body text, default blue, cream backgrounds, purple-gradient-on-white, identical layouts back to back.
clearFlip(slide)after every slide;await waitForLayoutUpdate()before geometry reads.- The flow is part of "done": no final checkpoint without
wireDeckFlow.jshaving run. - Checkpoints per §11 — "looks good" approves only the batch just shown.
8. Domain Architecture
Page Deck — <title> → slide boards on a 4-column contact grid (x = (i % 4)·2080, y = ⌊i/4⌋·1240)
→ each slide: root flex column with padding bound to deck.space.margin (96) → safe-area
containers (row / column / addGridLayout per archetype, spans on a 12-column grid: 4 = 544,
6 = 840, 8 = 1136) → texts (h1, eyebrow, p, stat, caption), shapes, img-placeholder
rects, component instances → a hidden notes text layer per slide (speaker notes convention) →
flow Deck from slide 01 + optional Section: <name> flows. Archetype catalogue and per-archetype
recipes: references/03-slide-archetypes.md.
9. Modes & Policies
Default review. Safe set here: renaming a slide board to the NN-archetype-slug convention.
Everything else (geometry, tokens, effects, flow wiring, uploads) is apply-with-review
(shared/modes-and-policies.md).
10. State Management
Ledger under RUN_ID (deck-<date>-<slug>): phase, deckPageId, deckStyle, tonalArc, motif,
outline[], slides:[{ n, id, name, archetype, done }], styleFrozen, flowName, exceptions[],
capabilities (verdicts for waitForLayoutUpdate, uploadMediaUrl, addGridLayout, backgroundBlur,
play-order direction), deckQuality, antiSlop (pref, direction, reports[] when strict mode is
on; the opt-in itself is file-wide plugin data penpot-ai → prefs.antiSlop). storage.deck mirrors the ids for the session. Resume: re-read
the ledger, re-run createDeckPage.js PHASE=verify, re-derive the slide list by name
(^\d{2}-), continue from the first done: false slide (shared/state-management.md).
11. User Checkpoints
| After phase | Artifacts | Ask |
|---|---|---|
| 0 (pref unset) | the anti-slop opt-in question (shared/anti-slop.md §0) | Avoid the generic-AI look? |
| 0 | outline table + style + tonal arc + motif + slide count (+ direction block if strict) | Approve direction? |
| 1 | token list (aliased / new / literal) + fonts found | Approve deck system? |
| 2 (style freeze) | exports of slides 01–03 | Freeze this style? |
| each batch | exports of the batch's slides + variety check | Approve batch? |
| N+1 | flow summary + gate result + scored critique (7 axes + monotony) | Approve / iterate? PDF/share steps given. |
12. Naming Conventions
shared/naming-conventions.md plus: boards NN-archetype-slug (01-cover-launch,
07-big-number-retention); layers semantic (h1, h2, eyebrow, p, stat, caption,
img-placeholder, card, safe-area, notes); tokens deck.* (deck.bg.dark, deck.text.on-dark,
deck.accent, deck.font.size.title, deck.space.margin); flows Deck, Section: <name>;
RUN_ID deck-2026-09-10-a.
13. Anti-Rationalization Table
| Excuse | Why it's wrong | Countermeasure (halt) |
|---|---|---|
| "I'll build all twelve slides in one call — it's the same code." | One-shot decks are unauditable and every slide comes out the same. | One slide per call; batches of 3–5; export + look before each checkpoint. |
| "An accent line under the title looks polished." | It is the hallmark of AI-generated slides. | No lines/bars under titles — hierarchy through scale and weight. |
| "Same layout on every content slide is consistency." | Consistency is the style; monotony is the layout. | Variety rule: change archetype, anchor or tone; the gate counts consecutive repeats. |
| "A plausible stat makes the point stronger." | Fabricated evidence (shared/design-quality.md §6). | Numbers only from the brief, with a source line; otherwise a labeled placeholder. |
| "This slide is fine as text only." | Text-only slides are the reading-not-presenting tell. | Add the archetype's mandatory visual or change archetype. |
| "I'll skip the flow; the user can click through boards." | Without a flow there is no deck, only boards. | wireDeckFlow.js runs before "done". |
| "I'll grab a stock image URL that surely exists." | Guessed URLs are fabricated content and fail upload silently. | Placeholder unless the user supplied the URL. |
| "24 px body reads fine on my monitor." | It is illegible from the back of a room at 1920×1080. | Body ≥ 28 px, floor 20 px; the gate enforces it. |
| "The default white board is fine for a light slide." | Unbound literal — never flips, off-system. | fills = [] then bind deck.bg.light. |
| "Centered everything is the safe choice." | Center is the no-decision alignment; content huddles. | Full canvas, 70/30 asymmetry, anchor varies per slide; center only cover/quote/closing. |
| "The page switch worked, I'll start creating boards." | openPage is async; boards land on the old page and cannot be moved. | createDeckPage.js PHASE=verify must return ok: true first. |
14. Helper Code Snippets
// Slide frame — the surface + bound margin (one slide per call; see scripts/buildSlide.js)
const slide = penpot.createBoard();
slide.name = "05-big-number-retention";
slide.resize(1920, 1080);
slide.clipContent = true; slide.showInViewMode = true;
const flex = slide.addFlexLayout();
flex.dir = "column"; flex.horizontalSizing = "fix"; flex.verticalSizing = "fix";
slide.fills = []; // drop Penpot's default opaque white
const bg = penpotUtils.findTokenByName("deck.bg.dark");
if (bg) slide.applyToken(bg, ["fill"]);
const margin = penpotUtils.findTokenByName("deck.space.margin");
for (const side of ["paddingTop", "paddingBottom", "paddingLeft", "paddingRight"]) {
try { slide.applyToken(margin, [side]); } catch { flex[side.replace("padding", "").toLowerCase() + "Padding"] = 96; }
}
penpot.currentPage.root.appendChild(slide);
// Exact font + size re-assert (gotcha #13b / #13)
const font = penpot.fonts.all.find(f => f.name === "Inter");
const variant = font && font.variants.find(v => v.fontWeight === "700");
const h1 = penpot.createText("Retention doubled in Q3");
h1.name = "h1";
if (font) font.applyToText(h1, variant);
h1.fontSize = "88"; h1.lineHeight = "1.1"; h1.letterSpacing = "-1.5"; h1.growType = "auto-height";
// Click → next slide with a slide-in animation (scripts/wireDeckFlow.js does this for every pair)
current.addInteraction("click", {
type: "navigate-to", destination: next,
animation: { type: "slide", way: "in", direction: "left", duration: 300, easing: "ease-out" }
});
// clearFlip — run before returning from every slide build (gotcha #16)
const clearFlip = (sh) => { let n = 0; const walk = (s) => { if (s.flipX) { s.flipX = false; n++; } (s.children || []).forEach(walk); }; walk(sh); return n; };
15. Reference Resources
penpot_api_info("Page", "createFlow"),penpot_api_info("Shape", "addInteraction"),penpot_api_info("Board", "waitForLayoutUpdate"),penpot_api_info("Board", "backgroundBlur"),penpot_api_info("Penpot", "uploadMediaUrl"),penpot_api_info("GridLayout"),penpot_api_info("Gradient").- Penpot View mode (
G V, ←/→, Shift+F, flows dropdown, share link) and "Export selected boards as PDF" (Penpot ≥ 2.12) —references/06-flow-and-playback.md. - Inspiration only (not a dependency): the official "Penpot slides template" on Penpot Hub (CC-BY-4.0).
16. Supporting Files
references/: 01-deck-brief-analysis.md (Phase 0), 02-deck-styles.md (Phase 0), 03-slide-archetypes.md (Phases 0, 2..N), 04-typography-and-grid.md (Phases 1..N), 05-visuals-and-effects.md (Phases 2..N), 06-flow-and-playback.md (Phase N+1), 07-critique-framework.md (Phase N+1), 08-anti-rationalization.md, 09-error-recovery.md.
scripts/: createDeckPage.js (Phase 1, two calls), setupDeckSystem.js (Phase 1), buildSlide.js (Phases 2..N), applyDeckEffects.js (Phases 2..N), wireDeckFlow.js (Phase N+1), auditDeckQuality.js (Phase N+1).
shared/: visual-effects.md (gradients, glass, shadows, images, clearFlip), design-quality.md (§2 type craft, §3 colour, §6 content honesty, §7 tells, §8 scoring procedure).
Doctrine paths. shared/… and policies/… resolve inside this bundle in native installs (vendored by the installer); in a Claude Code plugin install they live at the plugin root — ${CLAUDE_PLUGIN_ROOT}/shared/…, two directories up from this file.