Skip to main content

Write PPTX

Controlled node

The Write PPTX node (WritePPTXFile) turns a JSON presentation definition into a real PowerPoint .pptx file and saves it into the project library.

You describe the deck as data — slides, and on each slide a flat list of positioned elements (text, images, shapes, lines, tables, charts). The node validates that description against a strict schema, renders it with PptxGenJS, and uploads the result.

  • Node category: IO
  • Node label: Write PPTX
  • Internal name: WritePPTXFile

Contents

  1. Node interface
  2. Quick start
  3. The presentation definition
  4. Coordinates, layouts and the slide grid
  5. Slides
  6. Element reference
  7. Styling: defaults, roles and the merge order
  8. Images and the assets array
  9. Limits
  10. Examples
  11. Generating decks programmatically
  12. Troubleshooting
  13. Full JSON shape cheat sheet

1. Node interface

Inputs

PortKindRequiredDescription
runEventyesExecutes the node.
dataDatayesThe presentation definition. Either an object, or a JSON string that parses to one. Arrays are rejected.
nameData / text fieldyesOutput file name. .pptx is appended automatically if you leave it off. Must be a non-empty string.
pathData / text fieldnoLibrary folder path, e.g. reports/2026/q1. Folders are created if they don't exist. Empty → library root.

name and path can be typed directly into the node's text fields or wired in. A wired input always wins over the typed value.

Outputs

PortKindDescription
doneEventFires after the node runs — including when it fails.
fileDataOn success: { id, name, mimeType, size }. On failure: { error: "..." }.

Success payload:

{
"id": "-O0abcDEFghi123", // library document id
"name": "quarterly-review.pptx",
"mimeType": "application/vnd.openxmlformats-officedocument.presentationml.presentation",
"size": 48213 // bytes
}
warning

Always branch on file.error. The done event fires on both the happy and the sad path, so a downstream node that just consumes file will silently receive an error object. A Get Value / Run Filtrex Code check on error is the usual guard.

Error messages the node can return

file.errorCause
No project ID foundThe workflow has no runtime project — an environment problem, not a data problem.
No data provideddata was null/undefined.
Data is a string but not valid JSONdata was a string that failed JSON.parse.
Data must be a presentation definition objectdata was an array, a number, etc.
Invalid file namename missing, not a string, or blank.
Error creating PPTX: Presentation schema validation failed.The definition broke the schema — see Troubleshooting.
Error creating PPTX: <specific message>Bounds, limits, asset, shape or chart error. These messages are specific and actionable.
Error creating PPTX - empty bufferRenderer produced nothing (internal).
Error uploading PPTX file to libraryRender succeeded, storage write failed.

2. Quick start

The smallest deck that renders:

{
"slides": [
{
"elements": [
{
"type": "text",
"text": "Hello, world",
"position": { "x": 1, "y": 3, "w": 11.3, "h": 1.5 },
"style": { "fontSize": 44, "bold": true, "align": "center" }
}
]
}
]
}

Only slides is required at the top level, only elements is required on a slide, and every element needs a type and a position.


3. The presentation definition

Top-level keys. No other keys are allowed — unknown keys are a validation error, not a warning.

KeyTypeRequiredNotes
slidesarrayyes1–250 slides.
versionintegernoIf present it must be exactly 1.
layoutstringnoLAYOUT_WIDE (default), LAYOUT_16x9, LAYOUT_16x10, LAYOUT_4x3.
metadataobjectnotitle, subject, author, company, revision (integer ≥ 1).
themeobjectnoheadFontFace (default Aptos Display), bodyFontFace (default Aptos), lang (default en-GB).
defaultsobjectnoPer-element-type default styles plus named text roles. See §7.
assetsarraynoDeclares the image handles this deck is allowed to use. See §8.
{
"version": 1,
"layout": "LAYOUT_WIDE",
"metadata": {
"title": "Quarterly Review",
"subject": "FY26 Q1",
"author": "Intellectible",
"company": "Intellectible Ltd",
"revision": 3
},
"theme": {
"headFontFace": "Aptos Display",
"bodyFontFace": "Aptos",
"lang": "en-GB"
},
"defaults": { /* ... */ },
"assets": [ /* ... */ ],
"slides": [ /* ... */ ]
}

Two rules that catch everyone

  1. Colours are 6 hex digits with no #. "1F2937" ✅ — "#1F2937" ❌, "fff" ❌, "red" ❌. This applies everywhere a colour appears.
  2. Strict objects. Every object in the schema is closed. A typo like fontsize or colour, or a PptxGenJS option that isn't in the tables below, fails validation.

4. Coordinates, layouts and the slide grid

All positions and sizes are inches, measured from the top-left corner of the slide.

"position": { "x": 0.6, "y": 1.4, "w": 6.0, "h": 3.2 }
  • x, y — top-left corner. ≥ 0.
  • w, h — width and height. Must be strictly greater than 0 (0 is rejected; use 0.01).
  • All four are required on every element.
  • x + w and y + h must fit inside the slide, or you get Element extends beyond the slide boundary.

Slide sizes

layoutWidthHeightUse for
LAYOUT_WIDE (default)13.333"7.5"Modern 16:9 decks.
LAYOUT_16x910"5.625"16:9 on the older 10" canvas.
LAYOUT_16x1010"6.25"16:10.
LAYOUT_4x310"7.5"Legacy projectors.

A grid that works on LAYOUT_WIDE

Copy these numbers and your decks will look consistent:

Regionxywh
Safe margin0.60.512.136.5
Title0.60.5512.130.9
Subtitle / kicker0.61.4512.130.5
Body (full width)0.62.112.134.6
Left column (of 2)0.62.15.874.6
Right column (of 2)6.872.15.874.6
Three columns0.6 / 4.84 / 9.092.13.844.6
Footer0.66.8512.130.35

Column maths for n columns with margin m and gutter g: w = (13.333 − 2m − (n−1)g) / n, and column i starts at x = m + i × (w + g).


5. Slides

{
"id": "agenda", // optional, your own reference; not rendered
"background": { "color": "0B1220", "transparency": 0 },
"notes": "Speaker notes go here.", // string, or array of strings joined by blank lines
"elements": [ /* ... */ ] // required (may be empty)
}
KeyTypeNotes
elementsarrayRequired. Up to 500 per slide. Rendered in array order — later elements draw on top.
idstringOptional label for your own tooling. Has no effect on output.
backgroundobject{ color, transparency? }. color required if background is present.
notesstring | string[]Speaker notes. An array is joined with a blank line between entries.
masterstring⚠️ Not usable from this node — see below.
warning

master will always fail. Slide masters must be registered server-side as trusted masters, and the Write PPTX node does not register any. Setting master produces Unknown trusted slide master "<name>". Use defaults + roles to get reusable styling instead.


6. Element reference

Every element is an object with a type, a position, an optional id, and type-specific keys.

typeRendersRequired keys
textA text boxposition, and exactly one of text / runs
imageAn embedded pictureassetId, position
shapeAny PptxGenJS shape, optionally with a labelshape, position
lineA straight line / connectorposition
tableA tablerows, position
chartA native PowerPoint chartchartType, series, position

6.1 text element

{
"type": "text",
"id": "slide1-title", // optional
"role": "title", // optional, looks up defaults.roles.title
"text": "Revenue is up 34%", // EITHER this...
"runs": [ /* ... */ ], // ...OR this. Never both, never neither.
"position": { "x": 0.6, "y": 0.55, "w": 12.13, "h": 0.9 },
"style": { "fontSize": 36, "bold": true, "color": "111827" }
}

text is a single string (\n gives you line breaks). runs is an array of styled fragments — use it for mixed formatting inside one box, and for bullet lists.

"runs": [
{ "text": "Revenue ", "options": { "color": "6B7280" } },
{ "text": "£4.2m", "options": { "bold": true, "color": "0B7285", "fontSize": 28 } },
{ "text": " this quarter", "options": { "breakLine": true } }
]

Each run is { "text": "...", "options": { ...text style... } }. Run options accept the same keys as style below.

Text style keys

KeyTypeNotes
fontFacestringe.g. "Aptos", "Arial". Font must exist on the viewer's machine.
fontSizenumber 1–400Points.
bold, italic, underline, strikeboolean
colorhex6Text colour.
highlighthex6Highlighter colour behind the glyphs.
alignleft | center | right | justifyHorizontal.
valigntop | middle | bottomVertical, within the box. Defaults to middle — set "top" for body copy in a tall box.
marginnumber 0–5, or [l, t, r, b]Inset in points. 0 for pixel-tight layouts.
breakLinebooleanOn a run: end the paragraph after it.
charSpacingnumber −20–200Tracking, in points.
paraSpaceBeforePt / paraSpaceAfterPtnumber 0–400⚠️ No effect — see known no-ops. Use lineSpacingMultiple or empty runs instead.
lineSpacingMultiplenumber 0.1–101.2 for comfortable body text.
isTextBoxbooleanTreat as a text box (enables autofit behaviour).
fitnone | shrink | resizeshrink = shrink text to fit the box.
transparencynumber 0–100Percent.
rotatenumber 0–359Degrees clockwise.
fill{ color, transparency }Box background.
lineline object (below)Box border.
shadowshadow object (below)
hyperlink{ url } or { slide } + optional tooltipSee below.
bullettrue, or bullet object (below)Turns the paragraph into a list item.

Bullet object

KeyTypeNotes
typebullet | numbernumber gives an ordered list.
codestringUnicode hex for a custom glyph, e.g. "25BA" (▶), "2713" (✓), "2716" (✖). Must be exactly 4 hex digits or it silently falls back to .
stylestringNumbering style: arabicPeriod (default), arabicParenR, alphaLcPeriod, alphaUcPeriod, romanLcPeriod, romanUcPeriod.
startAtnumber ≥ 1The number this list item shows. See the numbering note below.
indentnumber 0–500Gap between the bullet glyph and the text, in points. Default ≈ 27pt.
hangingnumber 0–500⚠️ No effect — see known no-ops.
info

Numbered lists don't auto-increment. Every numbered paragraph is written with an explicit start number, so a list built from runs will render as 1. 1. 1. unless you set startAt yourself on each item: {"type":"number","startAt":1}, {"type":"number","startAt":2}, and so on. When generating in code this is a one-liner — bullet: { type: 'number', startAt: i + 1 }.

info

There are no outline levels. indent widens the gap between the glyph and the text; it does not move the bullet itself, so it can't produce true sub-bullets. To show hierarchy, put the sub-list in its own text box shifted right, or give sub-items a different glyph via code.

Hyperlink object — exactly one of:

KeyTypeNotes
urlstringMust start with https://. http:// and mailto: are rejected. Max 2048 chars.
slidenumber 1–10000Jump to a slide number inside this deck.
tooltipstringOptional, max 500 chars, combines with either.

Line object (used by style.line, shapes, lines, table borders)

KeyTypeNotes
colorhex6
widthnumber 0–100Points.
transparencynumber 0–100
dashenumsolid, dash, dashDot, dot, lgDash, lgDashDot, lgDashDotDot, sysDash, sysDashDot, sysDashDotDot, sysDot
beginArrowType / endArrowTypestringnone, arrow, triangle, stealth, diamond, oval

Shadow object

KeyTypeNotes
typeouter | inner | noneRequired when shadow is present.
colorhex6
opacitynumber 0–1Note: 0–1, not 0–100.
blurnumber 0–100Points.
anglenumber 0–359Degrees.
distancenumber 0–100Points.

6.2 image element

{
"type": "image",
"assetId": "company-logo",
"position": { "x": 11.0, "y": 0.45, "w": 1.7, "h": 0.8 },
"style": { "fit": "contain", "altText": "Company logo" }
}

assetId is a handle, not a file path or URL. It must appear in the top-level assets array — see §8.

Style keyTypeNotes
fitcontain (default) | cover | stretchSee below.
altTextstringAccessibility description, max 2000 chars.
roundingbooleantrue crops the image to a circle.
transparencynumber 0–100
rotatenumber 0–359
shadowshadow object
hyperlink{ url } or { slide }Same rules as text hyperlinks.

Fit modes

ModeBehaviour
containWhole image fits inside w × h, aspect ratio kept, letterboxed. Safest default.
coverImage fills w × h, aspect ratio kept, overflow cropped. Good for hero/banner images.
stretchImage forced to exactly w × h. Distorts unless the ratio already matches.

6.3 shape element

{
"type": "shape",
"shape": "roundRect",
"position": { "x": 0.6, "y": 2.1, "w": 3.8, "h": 2.0 },
"style": {
"fill": { "color": "EEF2FF" },
"line": { "color": "C7D2FE", "width": 1 },
"shadow": { "type": "outer", "color": "000000", "opacity": 0.15, "blur": 8, "angle": 90, "distance": 3 }
},
"text": "34% growth",
"textStyle": { "fontSize": 20, "bold": true, "align": "center", "valign": "middle", "color": "3730A3" }
}
KeyTypeNotes
shapestringA PptxGenJS shape name — see the list below. Unknown names give Unsupported PptxGenJS shape: <name>.
styleshape stylefill, line, shadow, transparency, rotate, flipH, flipV.
textstringOptional label drawn in the same box.
textStyletext styleStyling for that label.

Notes:

  • The label is drawn as a separate text box occupying the same rectangle, so valign: "middle" and align: "center" are how you centre it.
  • role is not available on shape labels — set textStyle explicitly (or lean on defaults.text).
  • style.radius is accepted by the schema but ignored. For rounded corners use the roundRect shape.

Common shape names (179 are available; these are the ones you'll actually reach for):

PurposeNames
Boxesrect, roundRect, snip1Rect, snip2DiagRect, round2SameRect, bevel, frame, halfFrame, plaque
Basic geometryellipse, triangle, rtTriangle, diamond, pentagon, hexagon, heptagon, octagon, decagon, dodecagon, trapezoid, parallelogram, donut, pie, chord, arc, teardrop, heart, cloud, moon, sun, lightningBolt, smileyFace, plus, can, cube
ArrowsrightArrow, leftArrow, upArrow, downArrow, leftRightArrow, upDownArrow, quadArrow, bentArrow, uturnArrow, curvedRightArrow, curvedLeftArrow, curvedUpArrow, curvedDownArrow, notchedRightArrow, stripedRightArrow, swooshArrow, circularArrow
Process / bannerschevron, homePlate, ribbon, ribbon2, wave, doubleWave, verticalScroll, horizontalScroll, funnel, gear6, gear9
CalloutswedgeRectCallout, wedgeRoundRectCallout, wedgeEllipseCallout, cloudCallout, borderCallout1, accentCallout1, rightArrowCallout, leftArrowCallout, upArrowCallout, downArrowCallout
FlowchartflowChartProcess, flowChartDecision, flowChartTerminator, flowChartAlternateProcess, flowChartDocument, flowChartMultidocument, flowChartPredefinedProcess, flowChartInputOutput, flowChartPreparation, flowChartManualInput, flowChartManualOperation, flowChartConnector, flowChartOffpageConnector, flowChartMerge, flowChartSort, flowChartDelay, flowChartDisplay, flowChartMagneticDisk, flowChartInternalStorage
Stars & mathsstar4, star5, star6, star7, star8, star10, star12, star16, star24, star32, mathPlus, mathMinus, mathMultiply, mathDivide, mathEqual, mathNotEqual
Braces & miscleftBrace, rightBrace, bracePair, leftBracket, rightBracket, bracketPair, corner, diagStripe, noSmoking, blockArc, pieWedge, chartPlus, chartStar, chartX, irregularSeal1, irregularSeal2

6.4 line element

A line is a straight segment drawn from (x, y) to (x + w, y + h).

{
"type": "line",
"position": { "x": 0.6, "y": 1.35, "w": 12.13, "h": 0.01 },
"style": { "line": { "color": "E5E7EB", "width": 1.5 } }
}
  • style is the shape style object (fill, line, shadow, transparency, rotate, flipH, flipV) — the stroke itself lives in style.line.
  • Because w and h must both be > 0, a perfectly horizontal rule uses h: 0.01, and a perfectly vertical one uses w: 0.01.
  • Arrowheads come from style.line.beginArrowType / endArrowType.
  • To slope a line upward instead of downward, set style.flipV: true.

6.5 table element

{
"type": "table",
"position": { "x": 0.6, "y": 2.0, "w": 12.13, "h": 3.4 },
"rows": [
["Region", "Q1", "Q2"],
["EMEA", 1200, 1480],
["AMER", 980, 1120]
],
"style": {
"colW": [5.13, 3.5, 3.5],
"fontSize": 14,
"border": { "color": "E5E7EB", "width": 1 }
}
}

Cells can be:

FormExampleNotes
string"EMEA"
number1480Converted to text.
booleantrueConverted to "true" / "false".
nullnullRenders as an empty cell. Handy for sparse rows.
object{ "text": "Total", "options": { "bold": true } }Per-cell styling. text is required.

Per-cell options: fill, color, fontFace, fontSize, bold, italic, align, valign, margin, border, colspan, rowspan, breakLine.

Table style: color, fill, fontFace, fontSize, bold, italic, align, valign, margin, border, plus:

KeyTypeNotes
colWnumber, or number[]Column widths in inches. Array length should match your column count.
rowHnumber, or number[]Row heights in inches.
borderline object, or [top, right, bottom, left]A 4-array styles each edge separately.
autoFitbooleanFit the table to the given w.
autoPagebooleanOverflow onto new slides automatically.
autoPageRepeatHeaderbooleanRepeat header rows on each continuation slide.
autoPageHeaderRowsinteger 0–100How many rows count as the header.
autoPageLineWeight / autoPageCharWeightnumber −10–10Tune the row-height estimate used by autoPage.

Rows may be ragged (different lengths), but keep them equal unless you're using colspan — PowerPoint renders ragged rows unpredictably.


6.6 chart element

{
"type": "chart",
"chartType": "bar",
"position": { "x": 0.6, "y": 1.8, "w": 7.5, "h": 4.8 },
"series": [
{ "name": "Revenue", "labels": ["Q1", "Q2", "Q3", "Q4"], "values": [240, 310, 390, 510] }
],
"style": { "barDir": "col", "showValue": true, "chartColors": ["00ACFA"] }
}

chartType: area, bar, bar3d, bubble, bubble3d, doughnut, line, pie, radar, scatter.

Series — each entry needs all three of:

KeyTypeNotes
namestringLegend label.
labelsstring[]Category labels. labels.length must equal values.length.
valuesnumber[]Data points.
sizesnumber[]Optional, bubble charts only. Must match values.length.

Chart style (all optional):

GroupKeys
TitleshowTitle, title, titleAlign, titleBold, titleColor, titleFontFace, titleFontSize
LegendshowLegend, legendPos (b/l/r/t/tr), legendColor, legendFontFace, legendFontSize
Data labelsshowValue, showPercent, showSerName, showLabel, showLeaderLines, dataLabelColor, dataLabelFontBold, dataLabelFontFace, dataLabelFontItalic, dataLabelFontSize, dataLabelFormatCode, dataLabelPosition (b/bestFit/ctr/l/r/t/inEnd/outEnd)
AxescatAxisLabelColor, catAxisLabelFontFace, catAxisLabelFontSize, catAxisHidden, valAxisLabelColor, valAxisLabelFontFace, valAxisLabelFontSize, valAxisHidden
BarbarDir (col = vertical, bar = horizontal), barGrouping (clustered, stacked, percentStacked, standard)
LinelineSize, lineSmooth, lineDataSymbolSize
Pie / doughnutfirstSliceAng
RadarradarStyle (standard, marker, filled)
ColourchartColors (hex6[]), chartColorsOpacity, invertedColors
FramechartArea / plotArea{ fill, border: { color, pt }, roundedCorners }, shadow, layout: { x, y, w, h } (0–1 fractions)

Notes per chart type:

  • pie / doughnut — use a single series. chartColors maps to slices in order. showPercent: true is usually what you want.
  • bar / bar3dbarDir: "col" for vertical columns (the common case), "bar" for horizontal. Multiple series + barGrouping: "stacked" gives a stacked chart.
  • scatter / bubble / bubble3d — PowerPoint treats the first series as the X-axis values and each subsequent series as a Y set. So supply [{ name: "X", ... }, { name: "Series A", ... }]. labels are still required on every series; use the X values as strings for the first one.
  • radar — one label set, one or more series; radarStyle: "filled" looks best with 2–3 series.

7. Styling: defaults, roles and the merge order

defaults lets you set house style once instead of repeating it on every element.

"defaults": {
"text": { "fontFace": "Aptos", "color": "1F2937", "fontSize": 16, "margin": 0 },
"image": { "fit": "contain" },
"shape": { "line": { "color": "E5E7EB", "width": 1 } },
"line": { "line": { "color": "E5E7EB", "width": 1 } },
"table": { "fontSize": 13, "valign": "middle" },
"chart": { "showLegend": true, "legendPos": "b", "chartColors": ["00ACFA", "7C3AED", "F59E0B"] },
"roles": {
"title": { "fontSize": 34, "bold": true, "color": "111827" },
"subtitle": { "fontSize": 18, "color": "6B7280" },
"body": { "fontSize": 16, "lineSpacingMultiple": 1.25 },
"footnote": { "fontSize": 10, "color": "9CA3AF" }
}
}
  • defaults.text/image/shape/line/table/chart apply to every element of that type.
  • defaults.roles.<name> is text-only. A text element opts in with "role": "<name>".
  • Role names must match ^[A-Za-z0-9_-]{1,100}$; up to 100 roles.

Merge order (later wins)

ElementOrder
textdefaults.textdefaults.roles[role]position → element style
imagedefaults.image → element style (then position)
shapedefaults.shapeposition → element style (label: defaults.textpositiontextStyle)
linedefaults.lineposition → element style
tabledefaults.tableposition → element style
chartdefaults.chartposition → element style

Merging is deep for nested objects, so defaults.shape.line.color and an element's style.line.width combine rather than replace each other. Arrays replace wholesale.

Run-level options are not merged with defaults.text — a run's options are passed through as-is on top of the box-level options.

Known no-ops

A few keys pass validation but have no effect on the rendered file. They're listed here so you don't spend time tuning them:

KeyWhereWorkaround
paraSpaceBeforePt, paraSpaceAfterPttext style, run optionsUse lineSpacingMultiple, or insert an empty run ({"text":" ","options":{"fontSize":8,"breakLine":true}}) between paragraphs.
bullet.hangingtext style, run optionsUse bullet.indent.
radiusshape style, line styleUse the roundRect shape.
idany element, any slidePurely a label for your own tooling.
masterslideNot supported at all — see §5.

8. Images and the assets array

Images can't be referenced by URL or path. Instead you declare a lookup table of handles, and image elements reference those handles. This means a deck definition — even one written by an LLM — can only reach files you explicitly listed.

"assets": [
{ "assetId": "company-logo", "id": "-O0abcDEFghi123" },
{ "assetId": "hero", "id": "-O0xyzUVWlmn456" }
]
KeyMeaning
assetIdThe handle your image elements use. Any string you like. Must be unique.
idThe library document id of an image already in the project library.

Then:

{ "type": "image", "assetId": "hero", "position": { "x": 0, "y": 0, "w": 13.333, "h": 3.2 }, "style": { "fit": "cover" } }

Rules and gotchas

  • Supported formats: PNG, JPEG, GIF. SVG is rejected (use png, jpeg, or gif) because it is a scriptable format. WebP, BMP, TIFF, PDF are not supported.
  • The library file's stored MIME type must match its actual bytes. A JPEG uploaded with a image/png MIME gives MIME type "image/png" does not match detected type "image/jpeg".
  • Max 20 MB per image (hard ceiling 50 MB).
  • Each asset is downloaded once and cached for the whole render, so reusing a logo on 40 slides costs one fetch.
  • Referencing an undeclared handle gives Image element references undeclared asset handle "<id>".
  • Where do library ids come from? The file output of any upload/write node ({ id: ... }), a Find Library Files / List Library Files node, or a library picker in the UI.

9. Limits

Exceeding a limit throws Error creating PPTX: <limitName> exceeded: <actual> > <max>.

LimitDefaultHard max
Input JSON size2 MB10 MB
Output file size100 MB250 MB
Slides100250
Elements per slide250500
Total elements5,00020,000
Text chars per element100,000250,000
Total text chars1,000,0005,000,000
Table rows1,0005,000
Table columns50100
Total table cells20,000100,000
Chart series20100
Chart points per series5,00020,000
Image bytes20 MB50 MB

The node uses the default column. The hard-max column is the ceiling the renderer would allow if limits were overridden server-side.

The renderer also emits a non-fatal POSSIBLE_TEXT_OVERFLOW warning (server logs only) when a text box looks too small for its content and fit isn't shrink/resize.


10. Examples

Every example below is a complete, valid data payload — paste one straight into the node.

10.1 Title slide

{
"layout": "LAYOUT_WIDE",
"metadata": { "title": "FY26 Q1 Review", "author": "Intellectible" },
"slides": [
{
"background": { "color": "0B1220" },
"notes": "Welcome everyone. 60 seconds on why we're here.",
"elements": [
{
"type": "shape",
"shape": "rect",
"position": { "x": 0, "y": 3.55, "w": 1.6, "h": 0.08 },
"style": { "fill": { "color": "00ACFA" }, "line": { "width": 0 } }
},
{
"type": "text",
"text": "FY26 Q1 Review",
"position": { "x": 0.9, "y": 2.5, "w": 11.5, "h": 1.0 },
"style": { "fontSize": 48, "bold": true, "color": "FFFFFF", "valign": "bottom" }
},
{
"type": "text",
"text": "Revenue, retention and what we're doing next",
"position": { "x": 0.9, "y": 3.85, "w": 11.5, "h": 0.6 },
"style": { "fontSize": 20, "color": "9CA3AF" }
},
{
"type": "text",
"text": "6 August 2026 · Confidential",
"position": { "x": 0.9, "y": 6.6, "w": 11.5, "h": 0.4 },
"style": { "fontSize": 12, "color": "6B7280" }
}
]
}
]
}

10.2 Bulleted agenda

{
"slides": [
{
"elements": [
{
"type": "text",
"text": "Agenda",
"position": { "x": 0.6, "y": 0.55, "w": 12.13, "h": 0.9 },
"style": { "fontSize": 34, "bold": true, "color": "111827" }
},
{
"type": "line",
"position": { "x": 0.6, "y": 1.45, "w": 12.13, "h": 0.01 },
"style": { "line": { "color": "E5E7EB", "width": 1.5 } }
},
{
"type": "text",
"position": { "x": 0.8, "y": 1.9, "w": 11.7, "h": 4.6 },
"style": { "fontSize": 20, "color": "374151", "valign": "top", "lineSpacingMultiple": 1.8 },
"runs": [
{ "text": "Where we landed in Q1", "options": { "bullet": true, "breakLine": true } },
{ "text": "Revenue by region", "options": { "bullet": true, "breakLine": true } },
{ "text": "Retention and churn drivers", "options": { "bullet": true, "breakLine": true } },
{ "text": "Q2 priorities", "options": { "bullet": true, "breakLine": true } },
{ "text": "Questions", "options": { "bullet": true, "breakLine": true } }
]
}
]
}
]
}

10.3 Bullet variations — plain, numbered, lettered, custom glyph, faked nesting

Note the explicit startAt on each numbered item, and that "nesting" is done with a second text box shifted right rather than an indent level.

{
"slides": [
{
"elements": [
{
"type": "text",
"text": "Bullet styles",
"position": { "x": 0.6, "y": 0.4, "w": 12.13, "h": 0.7 },
"style": { "fontSize": 30, "bold": true, "valign": "middle" }
},

{ "type": "text", "text": "Plain", "position": { "x": 0.6, "y": 1.3, "w": 3.84, "h": 0.35 }, "style": { "fontSize": 13, "bold": true, "valign": "top", "color": "6B7280" } },
{
"type": "text",
"position": { "x": 0.6, "y": 1.7, "w": 3.84, "h": 1.6 },
"style": { "fontSize": 15, "valign": "top", "lineSpacingMultiple": 1.4 },
"runs": [
{ "text": "Discovery", "options": { "bullet": true, "breakLine": true } },
{ "text": "Build", "options": { "bullet": true, "breakLine": true } },
{ "text": "Launch", "options": { "bullet": true, "breakLine": true } }
]
},

{ "type": "text", "text": "Numbered", "position": { "x": 4.84, "y": 1.3, "w": 3.84, "h": 0.35 }, "style": { "fontSize": 13, "bold": true, "valign": "top", "color": "6B7280" } },
{
"type": "text",
"position": { "x": 4.84, "y": 1.7, "w": 3.84, "h": 1.6 },
"style": { "fontSize": 15, "valign": "top", "lineSpacingMultiple": 1.4 },
"runs": [
{ "text": "Draft the brief", "options": { "bullet": { "type": "number", "startAt": 1 }, "breakLine": true } },
{ "text": "Review with legal", "options": { "bullet": { "type": "number", "startAt": 2 }, "breakLine": true } },
{ "text": "Ship it", "options": { "bullet": { "type": "number", "startAt": 3 }, "breakLine": true } }
]
},

{ "type": "text", "text": "Lettered", "position": { "x": 9.09, "y": 1.3, "w": 3.84, "h": 0.35 }, "style": { "fontSize": 13, "bold": true, "valign": "top", "color": "6B7280" } },
{
"type": "text",
"position": { "x": 9.09, "y": 1.7, "w": 3.84, "h": 1.6 },
"style": { "fontSize": 15, "valign": "top", "lineSpacingMultiple": 1.4 },
"runs": [
{ "text": "Option one", "options": { "bullet": { "type": "number", "style": "alphaLcPeriod", "startAt": 1 }, "breakLine": true } },
{ "text": "Option two", "options": { "bullet": { "type": "number", "style": "alphaLcPeriod", "startAt": 2 }, "breakLine": true } },
{ "text": "Option three", "options": { "bullet": { "type": "number", "style": "alphaLcPeriod", "startAt": 3 }, "breakLine": true } }
]
},

{ "type": "text", "text": "Custom glyphs", "position": { "x": 0.6, "y": 3.7, "w": 3.84, "h": 0.35 }, "style": { "fontSize": 13, "bold": true, "valign": "top", "color": "6B7280" } },
{
"type": "text",
"position": { "x": 0.6, "y": 4.1, "w": 3.84, "h": 1.6 },
"style": { "fontSize": 15, "valign": "top", "lineSpacingMultiple": 1.4 },
"runs": [
{ "text": "Shipped", "options": { "bullet": { "code": "2713" }, "color": "047857", "breakLine": true } },
{ "text": "In progress", "options": { "bullet": { "code": "25BA" }, "color": "B45309", "breakLine": true } },
{ "text": "Blocked", "options": { "bullet": { "code": "2716" }, "color": "B91C1C", "breakLine": true } }
]
},

{ "type": "text", "text": "Faked nesting (second box, shifted right)", "position": { "x": 4.84, "y": 3.7, "w": 7.9, "h": 0.35 }, "style": { "fontSize": 13, "bold": true, "valign": "top", "color": "6B7280" } },
{
"type": "text",
"position": { "x": 4.84, "y": 4.1, "w": 7.9, "h": 0.5 },
"style": { "fontSize": 15, "valign": "top" },
"runs": [ { "text": "Expand into EMEA", "options": { "bullet": true, "breakLine": true } } ]
},
{
"type": "text",
"position": { "x": 5.34, "y": 4.6, "w": 7.4, "h": 1.1 },
"style": { "fontSize": 13, "valign": "top", "color": "6B7280", "lineSpacingMultiple": 1.3 },
"runs": [
{ "text": "Open a Dublin entity", "options": { "bullet": { "code": "2013" }, "breakLine": true } },
{ "text": "Hire two AEs", "options": { "bullet": { "code": "2013" }, "breakLine": true } }
]
}
]
}
]
}
{
"slides": [
{
"elements": [
{
"type": "text",
"position": { "x": 0.6, "y": 1.2, "w": 12.13, "h": 2.0 },
"style": { "fontSize": 28, "lineSpacingMultiple": 1.3, "color": "1F2937" },
"runs": [
{ "text": "We closed " },
{ "text": "£4.2m", "options": { "bold": true, "color": "047857", "fontSize": 34 } },
{ "text": " in new ARR — " },
{ "text": "34% above plan", "options": { "italic": true, "highlight": "FEF08A" } },
{ "text": ".", "options": { "breakLine": true } },
{
"text": "Read the full board pack",
"options": {
"fontSize": 16,
"color": "1D4ED8",
"underline": true,
"hyperlink": { "url": "https://example.com/board-pack", "tooltip": "Opens the board pack" }
}
}
]
},
{
"type": "text",
"text": "Jump to the appendix →",
"position": { "x": 0.6, "y": 6.6, "w": 4.0, "h": 0.4 },
"style": { "fontSize": 12, "color": "6B7280", "hyperlink": { "slide": 2 } }
}
]
},
{
"elements": [
{
"type": "text",
"text": "Appendix",
"position": { "x": 0.6, "y": 0.6, "w": 12.13, "h": 0.9 },
"style": { "fontSize": 34, "bold": true }
}
]
}
]
}

10.5 Two-column layout with a divider

{
"defaults": {
"text": { "fontFace": "Aptos", "color": "1F2937", "margin": 0 },
"roles": {
"title": { "fontSize": 32, "bold": true, "color": "111827" },
"h2": { "fontSize": 18, "bold": true, "color": "0B7285" },
"body": { "fontSize": 15, "lineSpacingMultiple": 1.25, "color": "374151", "valign": "top" }
}
},
"slides": [
{
"elements": [
{ "type": "text", "role": "title", "text": "What changed, and why", "position": { "x": 0.6, "y": 0.55, "w": 12.13, "h": 0.9 } },
{ "type": "line", "position": { "x": 6.665, "y": 1.8, "w": 0.01, "h": 4.6 }, "style": { "line": { "color": "E5E7EB", "width": 1 } } },

{ "type": "text", "role": "h2", "text": "What changed", "position": { "x": 0.6, "y": 1.8, "w": 5.87, "h": 0.4 } },
{ "type": "text", "role": "body", "text": "Self-serve signup replaced the sales-led trial for teams under 20 seats. Onboarding dropped from 9 days to 40 minutes, and activation is measured at first shared workspace rather than first login.", "position": { "x": 0.6, "y": 2.3, "w": 5.87, "h": 4.1 } },

{ "type": "text", "role": "h2", "text": "Why it worked", "position": { "x": 6.87, "y": 1.8, "w": 5.87, "h": 0.4 } },
{ "type": "text", "role": "body", "text": "The bottleneck was never pricing — it was the gap between intent and a working workspace. Removing the human step removed the gap. Larger deals still route to sales, so ACV held flat while volume tripled.", "position": { "x": 6.87, "y": 2.3, "w": 5.87, "h": 4.1 } }
]
}
]
}

10.6 KPI cards

{
"defaults": {
"text": { "fontFace": "Aptos", "margin": 0 },
"shape": { "line": { "width": 0 } }
},
"slides": [
{
"background": { "color": "F8FAFC" },
"elements": [
{ "type": "text", "text": "Q1 at a glance", "position": { "x": 0.6, "y": 0.55, "w": 12.13, "h": 0.8 }, "style": { "fontSize": 32, "bold": true, "color": "111827" } },

{ "type": "shape", "shape": "roundRect", "position": { "x": 0.6, "y": 1.8, "w": 3.84, "h": 2.2 }, "style": { "fill": { "color": "FFFFFF" }, "shadow": { "type": "outer", "color": "94A3B8", "opacity": 0.25, "blur": 10, "angle": 90, "distance": 3 } } },
{ "type": "text", "text": "New ARR", "position": { "x": 0.9, "y": 2.05, "w": 3.24, "h": 0.4 }, "style": { "fontSize": 13, "color": "64748B" } },
{ "type": "text", "text": "£4.2m", "position": { "x": 0.9, "y": 2.45, "w": 3.24, "h": 0.9 }, "style": { "fontSize": 40, "bold": true, "color": "0F172A" } },
{ "type": "text", "text": "▲ 34% vs plan", "position": { "x": 0.9, "y": 3.35, "w": 3.24, "h": 0.4 }, "style": { "fontSize": 13, "color": "047857" } },

{ "type": "shape", "shape": "roundRect", "position": { "x": 4.84, "y": 1.8, "w": 3.84, "h": 2.2 }, "style": { "fill": { "color": "FFFFFF" }, "shadow": { "type": "outer", "color": "94A3B8", "opacity": 0.25, "blur": 10, "angle": 90, "distance": 3 } } },
{ "type": "text", "text": "Net retention", "position": { "x": 5.14, "y": 2.05, "w": 3.24, "h": 0.4 }, "style": { "fontSize": 13, "color": "64748B" } },
{ "type": "text", "text": "112%", "position": { "x": 5.14, "y": 2.45, "w": 3.24, "h": 0.9 }, "style": { "fontSize": 40, "bold": true, "color": "0F172A" } },
{ "type": "text", "text": "▲ 6 pts QoQ", "position": { "x": 5.14, "y": 3.35, "w": 3.24, "h": 0.4 }, "style": { "fontSize": 13, "color": "047857" } },

{ "type": "shape", "shape": "roundRect", "position": { "x": 9.09, "y": 1.8, "w": 3.84, "h": 2.2 }, "style": { "fill": { "color": "FFFFFF" }, "shadow": { "type": "outer", "color": "94A3B8", "opacity": 0.25, "blur": 10, "angle": 90, "distance": 3 } } },
{ "type": "text", "text": "Gross churn", "position": { "x": 9.39, "y": 2.05, "w": 3.24, "h": 0.4 }, "style": { "fontSize": 13, "color": "64748B" } },
{ "type": "text", "text": "4.1%", "position": { "x": 9.39, "y": 2.45, "w": 3.24, "h": 0.9 }, "style": { "fontSize": 40, "bold": true, "color": "0F172A" } },
{ "type": "text", "text": "▼ 0.8 pts QoQ", "position": { "x": 9.39, "y": 3.35, "w": 3.24, "h": 0.4 }, "style": { "fontSize": 13, "color": "B91C1C" } }
]
}
]
}

10.7 Shapes with labels

{
"slides": [
{
"elements": [
{ "type": "text", "text": "Shape gallery", "position": { "x": 0.6, "y": 0.4, "w": 12.13, "h": 0.7 }, "style": { "fontSize": 28, "bold": true } },

{ "type": "shape", "shape": "ellipse", "position": { "x": 0.6, "y": 1.4, "w": 2.4, "h": 2.4 }, "style": { "fill": { "color": "DBEAFE" }, "line": { "color": "3B82F6", "width": 2 } }, "text": "ellipse", "textStyle": { "align": "center", "valign": "middle", "fontSize": 14, "color": "1E3A8A" } },
{ "type": "shape", "shape": "hexagon", "position": { "x": 3.4, "y": 1.4, "w": 2.4, "h": 2.4 }, "style": { "fill": { "color": "DCFCE7" }, "line": { "color": "22C55E", "width": 2 } }, "text": "hexagon", "textStyle": { "align": "center", "valign": "middle", "fontSize": 14, "color": "14532D" } },
{ "type": "shape", "shape": "star5", "position": { "x": 6.2, "y": 1.4, "w": 2.4, "h": 2.4 }, "style": { "fill": { "color": "FEF3C7" }, "line": { "color": "F59E0B", "width": 2 } }, "text": "star5", "textStyle": { "align": "center", "valign": "middle", "fontSize": 12, "color": "78350F" } },
{ "type": "shape", "shape": "cloud", "position": { "x": 9.0, "y": 1.4, "w": 2.4, "h": 2.4 }, "style": { "fill": { "color": "F3E8FF" }, "line": { "color": "A855F7", "width": 2 } }, "text": "cloud", "textStyle": { "align": "center", "valign": "middle", "fontSize": 14, "color": "581C87" } },

{ "type": "shape", "shape": "wedgeRoundRectCallout", "position": { "x": 0.6, "y": 4.2, "w": 3.6, "h": 1.8 }, "style": { "fill": { "color": "FFFFFF" }, "line": { "color": "94A3B8", "width": 1.5 } }, "text": "A callout with a tail", "textStyle": { "align": "center", "valign": "middle", "fontSize": 14 } },
{ "type": "shape", "shape": "roundRect", "position": { "x": 4.6, "y": 4.2, "w": 3.6, "h": 1.8 }, "style": { "fill": { "color": "0F172A" }, "line": { "width": 0 }, "rotate": 350 }, "text": "roundRect, rotated 350°", "textStyle": { "align": "center", "valign": "middle", "fontSize": 14, "color": "FFFFFF", "rotate": 350 } },
{ "type": "shape", "shape": "can", "position": { "x": 8.6, "y": 4.2, "w": 2.2, "h": 1.8 }, "style": { "fill": { "color": "E2E8F0" }, "line": { "color": "64748B", "width": 1 } }, "text": "DB", "textStyle": { "align": "center", "valign": "middle", "fontSize": 16, "bold": true } }
]
}
]
}

10.8 Process flow with chevrons and arrows

{
"defaults": { "text": { "fontFace": "Aptos", "margin": 0 } },
"slides": [
{
"elements": [
{ "type": "text", "text": "Onboarding flow", "position": { "x": 0.6, "y": 0.5, "w": 12.13, "h": 0.7 }, "style": { "fontSize": 30, "bold": true } },

{ "type": "shape", "shape": "chevron", "position": { "x": 0.6, "y": 1.6, "w": 3.0, "h": 1.1 }, "style": { "fill": { "color": "0EA5E9" }, "line": { "width": 0 } }, "text": "Sign up", "textStyle": { "align": "center", "valign": "middle", "color": "FFFFFF", "fontSize": 16, "bold": true } },
{ "type": "shape", "shape": "chevron", "position": { "x": 3.4, "y": 1.6, "w": 3.0, "h": 1.1 }, "style": { "fill": { "color": "0284C7" }, "line": { "width": 0 } }, "text": "Verify email", "textStyle": { "align": "center", "valign": "middle", "color": "FFFFFF", "fontSize": 16, "bold": true } },
{ "type": "shape", "shape": "chevron", "position": { "x": 6.2, "y": 1.6, "w": 3.0, "h": 1.1 }, "style": { "fill": { "color": "0369A1" }, "line": { "width": 0 } }, "text": "Create workspace", "textStyle": { "align": "center", "valign": "middle", "color": "FFFFFF", "fontSize": 16, "bold": true } },
{ "type": "shape", "shape": "chevron", "position": { "x": 9.0, "y": 1.6, "w": 3.0, "h": 1.1 }, "style": { "fill": { "color": "075985" }, "line": { "width": 0 } }, "text": "Invite team", "textStyle": { "align": "center", "valign": "middle", "color": "FFFFFF", "fontSize": 16, "bold": true } },

{ "type": "shape", "shape": "flowChartTerminator", "position": { "x": 0.6, "y": 3.6, "w": 2.6, "h": 1.0 }, "style": { "fill": { "color": "F1F5F9" }, "line": { "color": "94A3B8", "width": 1 } }, "text": "Start", "textStyle": { "align": "center", "valign": "middle", "fontSize": 14 } },
{ "type": "line", "position": { "x": 3.3, "y": 4.1, "w": 1.0, "h": 0.01 }, "style": { "line": { "color": "64748B", "width": 2, "endArrowType": "triangle" } } },
{ "type": "shape", "shape": "flowChartDecision", "position": { "x": 4.4, "y": 3.3, "w": 2.6, "h": 1.6 }, "style": { "fill": { "color": "FEF9C3" }, "line": { "color": "CA8A04", "width": 1 } }, "text": "Verified?", "textStyle": { "align": "center", "valign": "middle", "fontSize": 14 } },
{ "type": "line", "position": { "x": 7.1, "y": 4.1, "w": 1.0, "h": 0.01 }, "style": { "line": { "color": "64748B", "width": 2, "endArrowType": "triangle" } } },
{ "type": "shape", "shape": "flowChartProcess", "position": { "x": 8.2, "y": 3.6, "w": 2.6, "h": 1.0 }, "style": { "fill": { "color": "DCFCE7" }, "line": { "color": "16A34A", "width": 1 } }, "text": "Activate", "textStyle": { "align": "center", "valign": "middle", "fontSize": 14 } },

{ "type": "line", "position": { "x": 5.7, "y": 4.9, "w": 0.01, "h": 1.0 }, "style": { "line": { "color": "B91C1C", "width": 2, "dash": "dash", "endArrowType": "triangle" } } },
{ "type": "text", "text": "Resend verification", "position": { "x": 4.4, "y": 5.95, "w": 2.6, "h": 0.4 }, "style": { "fontSize": 12, "color": "B91C1C", "align": "center" } }
]
}
]
}

10.9 Line styles reference

{
"slides": [
{
"elements": [
{ "type": "text", "text": "Lines and connectors", "position": { "x": 0.6, "y": 0.5, "w": 12.13, "h": 0.7 }, "style": { "fontSize": 28, "bold": true } },

{ "type": "text", "text": "solid", "position": { "x": 0.6, "y": 1.6, "w": 2.0, "h": 0.3 }, "style": { "fontSize": 12, "color": "6B7280" } },
{ "type": "line", "position": { "x": 2.8, "y": 1.75, "w": 9.9, "h": 0.01 }, "style": { "line": { "color": "0F172A", "width": 2 } } },

{ "type": "text", "text": "dash", "position": { "x": 0.6, "y": 2.3, "w": 2.0, "h": 0.3 }, "style": { "fontSize": 12, "color": "6B7280" } },
{ "type": "line", "position": { "x": 2.8, "y": 2.45, "w": 9.9, "h": 0.01 }, "style": { "line": { "color": "0F172A", "width": 2, "dash": "dash" } } },

{ "type": "text", "text": "dot", "position": { "x": 0.6, "y": 3.0, "w": 2.0, "h": 0.3 }, "style": { "fontSize": 12, "color": "6B7280" } },
{ "type": "line", "position": { "x": 2.8, "y": 3.15, "w": 9.9, "h": 0.01 }, "style": { "line": { "color": "0F172A", "width": 2, "dash": "dot" } } },

{ "type": "text", "text": "arrow (end)", "position": { "x": 0.6, "y": 3.7, "w": 2.0, "h": 0.3 }, "style": { "fontSize": 12, "color": "6B7280" } },
{ "type": "line", "position": { "x": 2.8, "y": 3.85, "w": 9.9, "h": 0.01 }, "style": { "line": { "color": "0EA5E9", "width": 3, "endArrowType": "triangle" } } },

{ "type": "text", "text": "arrow (both)", "position": { "x": 0.6, "y": 4.4, "w": 2.0, "h": 0.3 }, "style": { "fontSize": 12, "color": "6B7280" } },
{ "type": "line", "position": { "x": 2.8, "y": 4.55, "w": 9.9, "h": 0.01 }, "style": { "line": { "color": "7C3AED", "width": 3, "beginArrowType": "oval", "endArrowType": "stealth" } } },

{ "type": "text", "text": "diagonal", "position": { "x": 0.6, "y": 5.1, "w": 2.0, "h": 0.3 }, "style": { "fontSize": 12, "color": "6B7280" } },
{ "type": "line", "position": { "x": 2.8, "y": 5.2, "w": 4.0, "h": 1.4 }, "style": { "line": { "color": "16A34A", "width": 2, "endArrowType": "arrow" } } },

{ "type": "text", "text": "diagonal, flipV", "position": { "x": 7.2, "y": 5.1, "w": 2.0, "h": 0.3 }, "style": { "fontSize": 12, "color": "6B7280" } },
{ "type": "line", "position": { "x": 9.0, "y": 5.2, "w": 3.7, "h": 1.4 }, "style": { "flipV": true, "line": { "color": "B45309", "width": 2, "endArrowType": "arrow" } } }
]
}
]
}

10.10 Table with a styled header row

colW sums to the table's w (4.13 + 2 + 2 + 2 + 2 = 12.13). Per-cell options style the header, and the total row uses bold plus its own fill.

{
"defaults": {
"table": { "fontFace": "Aptos", "fontSize": 13, "valign": "middle", "border": { "color": "E5E7EB", "width": 1 } }
},
"slides": [
{
"elements": [
{ "type": "text", "text": "Revenue by region", "position": { "x": 0.6, "y": 0.55, "w": 12.13, "h": 0.8 }, "style": { "fontSize": 30, "bold": true, "color": "111827" } },
{
"type": "table",
"position": { "x": 0.6, "y": 1.6, "w": 12.13, "h": 4.2 },
"rows": [
[
{ "text": "Region", "options": { "bold": true, "color": "FFFFFF", "fill": { "color": "0F172A" } } },
{ "text": "Q1", "options": { "bold": true, "color": "FFFFFF", "fill": { "color": "0F172A" }, "align": "right" } },
{ "text": "Q2", "options": { "bold": true, "color": "FFFFFF", "fill": { "color": "0F172A" }, "align": "right" } },
{ "text": "Q3", "options": { "bold": true, "color": "FFFFFF", "fill": { "color": "0F172A" }, "align": "right" } },
{ "text": "QoQ", "options": { "bold": true, "color": "FFFFFF", "fill": { "color": "0F172A" }, "align": "right" } }
],
["EMEA", "1,200", "1,480", "1,910", { "text": "+29%", "options": { "align": "right", "color": "047857" } }],
["AMER", "980", "1,120", "1,340", { "text": "+20%", "options": { "align": "right", "color": "047857" } }],
["APAC", "410", "520", "505", { "text": "−3%", "options": { "align": "right", "color": "B91C1C" } }],
["LATAM", "140", "180", null, { "text": "n/a", "options": { "align": "right", "color": "9CA3AF" } }],
[
{ "text": "Total", "options": { "bold": true, "fill": { "color": "F1F5F9" } } },
{ "text": "2,730", "options": { "bold": true, "align": "right", "fill": { "color": "F1F5F9" } } },
{ "text": "3,300", "options": { "bold": true, "align": "right", "fill": { "color": "F1F5F9" } } },
{ "text": "3,755", "options": { "bold": true, "align": "right", "fill": { "color": "F1F5F9" } } },
{ "text": "+14%", "options": { "bold": true, "align": "right", "fill": { "color": "F1F5F9" } } }
]
],
"style": { "colW": [4.13, 2, 2, 2, 2], "rowH": 0.6, "align": "right", "margin": 6 }
},
{ "type": "text", "text": "Figures in £000s. LATAM Q3 not yet closed.", "position": { "x": 0.6, "y": 6.0, "w": 12.13, "h": 0.35 }, "style": { "fontSize": 11, "color": "9CA3AF", "valign": "top" } }
]
}
]
}

For a table that is longer than one slide, drop the fixed h guesswork and let it flow:

"style": { "colW": [4.13, 2, 2, 2, 2], "autoPage": true, "autoPageRepeatHeader": true, "autoPageHeaderRows": 1 }

10.11 Charts: column, pie and multi-series line

Three slides, one chart each. Note chartColors ordering on the pie (one colour per slice) and the labels/values lengths matching on every series.

{
"defaults": {
"chart": { "showLegend": true, "legendPos": "b", "chartColors": ["00ACFA", "7C3AED", "F59E0B", "10B981"] }
},
"slides": [
{
"elements": [
{ "type": "text", "text": "New ARR by quarter", "position": { "x": 0.6, "y": 0.5, "w": 12.13, "h": 0.7 }, "style": { "fontSize": 28, "bold": true } },
{
"type": "chart",
"chartType": "bar",
"position": { "x": 0.6, "y": 1.4, "w": 12.13, "h": 5.2 },
"series": [
{ "name": "FY25", "labels": ["Q1", "Q2", "Q3", "Q4"], "values": [180, 210, 260, 300] },
{ "name": "FY26", "labels": ["Q1", "Q2", "Q3", "Q4"], "values": [240, 310, 390, 510] }
],
"style": {
"barDir": "col",
"barGrouping": "clustered",
"showValue": true,
"dataLabelFontSize": 10,
"dataLabelPosition": "outEnd",
"catAxisLabelFontSize": 12,
"valAxisLabelFontSize": 12
}
}
]
},
{
"elements": [
{ "type": "text", "text": "Revenue mix", "position": { "x": 0.6, "y": 0.5, "w": 12.13, "h": 0.7 }, "style": { "fontSize": 28, "bold": true } },
{
"type": "chart",
"chartType": "doughnut",
"position": { "x": 0.6, "y": 1.4, "w": 6.0, "h": 5.2 },
"series": [
{ "name": "Share", "labels": ["EMEA", "AMER", "APAC", "LATAM"], "values": [1910, 1340, 505, 180] }
],
"style": { "showPercent": true, "showLegend": true, "legendPos": "r", "dataLabelColor": "FFFFFF", "dataLabelFontSize": 12 }
},
{
"type": "text",
"position": { "x": 7.0, "y": 1.8, "w": 5.73, "h": 4.0 },
"style": { "fontSize": 16, "valign": "top", "color": "374151", "lineSpacingMultiple": 1.4 },
"runs": [
{ "text": "EMEA is now half the book", "options": { "bullet": true, "breakLine": true } },
{ "text": "APAC flat after the pricing change", "options": { "bullet": true, "breakLine": true } },
{ "text": "LATAM still sub-scale", "options": { "bullet": true, "breakLine": true } }
]
}
]
},
{
"elements": [
{ "type": "text", "text": "Retention trend", "position": { "x": 0.6, "y": 0.5, "w": 12.13, "h": 0.7 }, "style": { "fontSize": 28, "bold": true } },
{
"type": "chart",
"chartType": "line",
"position": { "x": 0.6, "y": 1.4, "w": 12.13, "h": 5.2 },
"series": [
{ "name": "Gross retention", "labels": ["Jan", "Feb", "Mar", "Apr", "May", "Jun"], "values": [94, 94.5, 95, 95.2, 95.8, 96.1] },
{ "name": "Net retention", "labels": ["Jan", "Feb", "Mar", "Apr", "May", "Jun"], "values": [104, 106, 107, 109, 111, 112] }
],
"style": { "lineSize": 3, "lineSmooth": true, "lineDataSymbolSize": 8, "valAxisLabelFontSize": 12, "catAxisLabelFontSize": 12 }
}
]
}
]
}

assets declares two handles; the elements reference them by handle. The same logo handle is used on both slides and is fetched once.

{
"assets": [
{ "assetId": "hero", "id": "-O0xyzUVWlmn456" },
{ "assetId": "logo", "id": "-O0abcDEFghi123" }
],
"slides": [
{
"elements": [
{ "type": "image", "assetId": "hero", "position": { "x": 0, "y": 0, "w": 13.333, "h": 3.0 }, "style": { "fit": "cover", "altText": "Team at the Q1 offsite" } },
{ "type": "text", "text": "FY26 Q1 Review", "position": { "x": 0.6, "y": 3.4, "w": 12.13, "h": 1.0 }, "style": { "fontSize": 44, "bold": true, "color": "0F172A" } },
{ "type": "image", "assetId": "logo", "position": { "x": 11.03, "y": 6.5, "w": 1.7, "h": 0.7 }, "style": { "fit": "contain", "altText": "Company logo" } }
]
},
{
"elements": [
{ "type": "text", "text": "Where we spent the quarter", "position": { "x": 0.6, "y": 0.55, "w": 12.13, "h": 0.9 }, "style": { "fontSize": 32, "bold": true } },
{ "type": "image", "assetId": "hero", "position": { "x": 0.6, "y": 1.7, "w": 5.87, "h": 3.6 }, "style": { "fit": "contain", "rounding": false, "shadow": { "type": "outer", "color": "94A3B8", "opacity": 0.3, "blur": 12, "angle": 90, "distance": 4 } } },
{ "type": "text", "text": "Two thirds of engineering time went into the self-serve funnel. The rest went to reliability work that unblocked the enterprise pipeline.", "position": { "x": 6.87, "y": 1.7, "w": 5.87, "h": 3.6 }, "style": { "fontSize": 17, "valign": "top", "color": "374151", "lineSpacingMultiple": 1.35 } },
{ "type": "image", "assetId": "logo", "position": { "x": 11.03, "y": 6.5, "w": 1.7, "h": 0.7 }, "style": { "fit": "contain" } }
]
}
]
}

11. Generating decks programmatically

Hand-writing coordinates is fine for a fixed template. As soon as the number of slides depends on the data, generate the definition in code and wire it into data.

The shape that works

Use a Run JavaScript Code (or Run Python Code) node to turn records into a presentation definition, then feed its result.output into Data:

Start → (fetch/extract data) → Run JavaScript Code → Write PPTX → Done

The code node reads inputs.json and writes output.json, so result.output is already the object Write PPTX wants — no JSON string parsing needed on either side.

// Run JavaScript Code — one slide per region, built from inputs.regions
const fs = require('fs');
const inputs = JSON.parse(fs.readFileSync('inputs.json', 'utf8'));

const M = 0.6; // margin
const SLIDE_W = 13.333;
const CONTENT_W = SLIDE_W - 2 * M; // 12.13

const deck = {
version: 1,
layout: 'LAYOUT_WIDE',
metadata: { title: 'Regional review', author: 'Intellectible' },
defaults: {
text: { fontFace: 'Aptos', color: '1F2937', margin: 0 },
roles: {
title: { fontSize: 32, bold: true, color: '111827' },
body: { fontSize: 16, valign: 'top', lineSpacingMultiple: 1.3, color: '374151' },
},
},
slides: [],
};

for (const region of inputs.regions) {
deck.slides.push({
id: `region-${region.code}`,
notes: region.talkTrack || '',
elements: [
{ type: 'text', role: 'title', text: region.name,
position: { x: M, y: 0.55, w: CONTENT_W, h: 0.9 } },
{ type: 'line',
position: { x: M, y: 1.45, w: CONTENT_W, h: 0.01 },
style: { line: { color: 'E5E7EB', width: 1.5 } } },
// numbered list: startAt must be set explicitly on every item
{ type: 'text',
position: { x: M + 0.2, y: 1.9, w: CONTENT_W - 0.2, h: 3.0 },
style: { fontSize: 18, valign: 'top', lineSpacingMultiple: 1.5 },
runs: region.highlights.map((h, i) => ({
text: h,
options: { bullet: { type: 'number', startAt: i + 1 }, breakLine: true },
})) },
{ type: 'chart', chartType: 'bar',
position: { x: M, y: 5.0, w: CONTENT_W, h: 1.9 },
series: [{ name: region.name, labels: region.quarters, values: region.values }],
style: { barDir: 'col', showValue: true } },
],
});
}

fs.writeFileSync('output.json', JSON.stringify(deck));

Helpers worth writing once

  • A column function. colX(i, n, gutter) returning M + i * (w + gutter) with w = (13.333 − 2M − (n−1) × gutter) / n removes every arithmetic slip in multi-column slides.
  • A bullets(items, opts) function that maps items to runs with breakLine: true and, for ordered lists, startAt: i + 1.
  • A card function returning the shape plus its label text boxes as an array, so a KPI row is [...card(0, 'New ARR', '£4.2m'), ...card(1, ...)].
  • A bounds assertion. Throw in your own code when x + w > 13.333 or y + h > 7.5; a failure you can see in stderr beats Element extends beyond the slide boundary. from the renderer.

Using AI to write the deck

Two approaches, and the second is much more reliable:

  1. AI writes the whole definition. Point AI Write Data at a schema describing the presentation definition. Workable for small decks, but the model has to get coordinates, bounds and every strict-object key right at once, and validation is all-or-nothing.
  2. AI writes content, code writes layout. Have AI Write Data emit a small content structure — e.g. { slides: [{ heading, bullets, note }] } — then let a Run JavaScript Code node place it on the grid. The model does what it is good at, the code guarantees the geometry, and adjusting the template later doesn't mean re-prompting.
tip

Whichever route you take, keep a Write JSON File node on the same definition while you are developing. When the render fails you can open the exact JSON that was submitted instead of guessing what the generator produced.


12. Troubleshooting

Presentation schema validation failed.

The definition didn't match the schema. In rough order of likelihood:

CheckFix
A colour with a #, 3 digits, or a nameUse 6 hex digits, no hash: "1F2937".
A misspelled or invented key (fontsize, colour, valignment)Every object is strict. Only the keys in the tables above are allowed.
A text element with both text and runs, or with neitherSupply exactly one.
w or h of 0Must be greater than 0 — use 0.01 for rules and hairlines.
A missing x, y, w or hAll four are required on every element.
version set to something other than 1Either omit it or set 1.
A slide missing elementsRequired, though it may be [].
shadow without typetype is required whenever shadow is present.
A hyperlink with http:// or mailto:Only https:// URLs, or { "slide": n }.
A bullet code that isn't 4 hex digitsUse e.g. "2713"; anything else silently falls back to .
A PptxGenJS option copied from elsewhereIf it isn't documented above, the schema rejects it.

The message doesn't name the offending key, so when a large generated deck fails, bisect it: render slides.slice(0, 1), then half, then narrow down.

The render succeeds but the slide looks wrong

SymptomCauseFix
Body text sits in the vertical middle of its boxvalign defaults to middleSet "valign": "top".
Text spills out of its boxNo autofit by default"fit": "shrink", or enlarge h. Server logs show a POSSIBLE_TEXT_OVERFLOW warning.
A numbered list renders 1. 1. 1.Numbers don't auto-incrementSet startAt on every item — startAt: i + 1.
A custom bullet shows as code wasn't exactly 4 hex digitsUse the 4-digit form, e.g. "25BA".
Sub-bullets don't indentThere are no outline levelsUse a second text box shifted right, or a different glyph.
Paragraph spacing has no effectparaSpaceBeforePt / paraSpaceAfterPt are no-opsUse lineSpacingMultiple, or an empty run between paragraphs.
A rect has square corners despite radiusradius is ignoredUse the roundRect shape.
A line is invisiblestyle.line.width is 0, or the stroke was put outside style.linePut colour and width in style.line.
A shape label isn't centredThe label is its own text box over the same rectangleSet align: "center" and valign: "middle" in textStyle.
A rotated shape's label isn't rotatedrotate on the shape doesn't rotate the labelSet rotate in textStyle too.
Fonts look wrong when someone else opens itFonts aren't embeddedStick to fonts your audience has, e.g. Aptos, Arial, Calibri.
An element is hidden behind anotherLater elements draw on topReorder the elements array.

Bounds, images, charts and limits

ErrorWhat to do
Element extends beyond the slide boundary.Check x + w against the slide width and y + h against its height for the layout you chose — LAYOUT_WIDE is 13.333 × 7.5, the others are 10" wide. Switching layout without redoing coordinates is the usual cause.
Image element references undeclared asset handle "<id>"Add the handle to the top-level assets array. assetId on the element must match assetId in assets exactly.
use png, jpeg, or gifThe library file is an SVG or another unsupported format. Convert it to PNG first.
MIME type "..." does not match detected type "..."The library file's stored MIME doesn't match its bytes. Re-upload the image with the correct type.
Unsupported PptxGenJS shape: <name>Typo or an invented shape name — they are case-sensitive (roundRect, not roundrect).
A chart is empty or mislabelledlabels.length must equal values.length on every series. For scatter/bubble, the first series supplies the X values.
<limitName> exceeded: <actual> > <max>.See §9. Split the deck, trim the data, or shorten the text.
Unknown trusted slide master "<name>"Remove master — this node registers no masters. Use defaults and roles.

Nothing rendered at all

ErrorMeaning
No data providedThe data input never received a value — check the wire and that the upstream node actually ran.
Data is a string but not valid JSONA string arrived that JSON.parse rejected, often a truncated LLM response or a double-encoded payload.
Data must be a presentation definition objectAn array arrived. Wrap it: { "slides": [...] }.
Invalid file namename is blank. It has no default.
No project ID foundThe workflow has no runtime project — not a data problem.
Error uploading PPTX file to libraryThe file rendered but storage rejected the write. Retry; check the library path.
warning

A downstream node receiving { error: "..." } instead of { id, name, ... } means the node failed — but done still fired. If your workflow "succeeds" yet no file appears in the library, inspect file rather than the event.


13. Full JSON shape cheat sheet

Every key the node accepts, in one skeleton. Everything except slides (and elements on a slide, type/position on an element) is optional.

{
"version": 1, // must be 1 if present
"layout": "LAYOUT_WIDE", // LAYOUT_WIDE | LAYOUT_16x9 | LAYOUT_16x10 | LAYOUT_4x3
"metadata": { "title": "", "subject": "", "author": "", "company": "", "revision": 1 },
"theme": { "headFontFace": "Aptos Display", "bodyFontFace": "Aptos", "lang": "en-GB" },

"defaults": {
"text": { /* any text style key */ },
"image": { /* any image style key */ },
"shape": { /* fill, line, shadow, transparency, rotate, flipH, flipV */ },
"line": { /* same as shape */ },
"table": { /* any table style key */ },
"chart": { /* any chart style key */ },
"roles": { "title": { /* text style */ }, "body": { /* text style */ } }
},

"assets": [
{ "assetId": "logo", "id": "<library document id>" } // png / jpeg / gif only
],

"slides": [ // 1–250
{
"id": "optional-label", // not rendered
"background": { "color": "FFFFFF", "transparency": 0 },
"notes": "speaker notes", // or ["para", "para"]
"elements": [ // required; up to 500; later draws on top

{ "type": "text",
"role": "title", // optional; looks up defaults.roles
"text": "single string", // EITHER text...
"runs": [ { "text": "", "options": { /* text style */ } } ], // ...OR runs
"position": { "x": 0, "y": 0, "w": 1, "h": 1 }, // w, h > 0
"style": {
"fontFace": "Aptos", "fontSize": 16,
"bold": false, "italic": false, "underline": false, "strike": false,
"color": "1F2937", "highlight": "FEF08A",
"align": "left", // left | center | right | justify
"valign": "middle", // top | middle | bottom (default middle)
"margin": 0, // number 0–5, or [l, t, r, b]
"breakLine": false, "charSpacing": 0, "lineSpacingMultiple": 1.2,
"isTextBox": true,
"fit": "none", // none | shrink | resize
"transparency": 0, "rotate": 0,
"fill": { "color": "FFFFFF", "transparency": 0 },
"line": { "color": "E5E7EB", "width": 1, "transparency": 0, "dash": "solid",
"beginArrowType": "none", "endArrowType": "none" },
"shadow": { "type": "outer", "color": "000000", "opacity": 0.2,
"blur": 8, "angle": 90, "distance": 3 },
"hyperlink": { "url": "https://...", "tooltip": "" }, // or { "slide": 2 }
"bullet": { "type": "bullet", "code": "2713", "style": "arabicPeriod",
"startAt": 1, "indent": 27 } // or bullet: true
} },

{ "type": "image",
"assetId": "logo", // must exist in assets
"position": { "x": 0, "y": 0, "w": 1, "h": 1 },
"style": { "fit": "contain", // contain | cover | stretch
"altText": "", "rounding": false,
"transparency": 0, "rotate": 0,
"shadow": { "type": "outer" },
"hyperlink": { "url": "https://..." } } },

{ "type": "shape",
"shape": "roundRect", // PptxGenJS shape name, case-sensitive
"position": { "x": 0, "y": 0, "w": 1, "h": 1 },
"style": { "fill": { "color": "EEF2FF", "transparency": 0 },
"line": { "color": "C7D2FE", "width": 1 },
"shadow": { "type": "outer" },
"transparency": 0, "rotate": 0, "flipH": false, "flipV": false },
"text": "optional label",
"textStyle": { /* any text style key */ } },

{ "type": "line",
"position": { "x": 0, "y": 0, "w": 1, "h": 0.01 }, // draws (x,y) → (x+w, y+h)
"style": { "line": { "color": "E5E7EB", "width": 1.5, "dash": "solid",
"beginArrowType": "none", "endArrowType": "triangle" },
"flipV": false } },

{ "type": "table",
"position": { "x": 0, "y": 0, "w": 1, "h": 1 },
"rows": [
["string", 123, true, null],
[ { "text": "styled", "options": { "bold": true, "colspan": 2, "rowspan": 1,
"fill": { "color": "F1F5F9" } } } ]
],
"style": { "colW": [1, 1], "rowH": 0.5,
"color": "1F2937", "fill": { "color": "FFFFFF" },
"fontFace": "Aptos", "fontSize": 13,
"bold": false, "italic": false, "align": "left", "valign": "middle",
"margin": 6,
"border": { "color": "E5E7EB", "width": 1 }, // or [top, right, bottom, left]
"autoFit": false, "autoPage": false,
"autoPageRepeatHeader": false, "autoPageHeaderRows": 1,
"autoPageLineWeight": 0, "autoPageCharWeight": 0 } },

{ "type": "chart",
"chartType": "bar", // area bar bar3d bubble bubble3d doughnut line pie radar scatter
"position": { "x": 0, "y": 0, "w": 1, "h": 1 },
"series": [
{ "name": "Series", "labels": ["A", "B"], "values": [1, 2], "sizes": [3, 4] }
], // labels.length === values.length
"style": { "showTitle": false, "title": "", "titleFontSize": 18,
"showLegend": true, "legendPos": "b",
"showValue": false, "showPercent": false,
"dataLabelPosition": "outEnd", "dataLabelFontSize": 10,
"catAxisHidden": false, "valAxisHidden": false,
"barDir": "col", "barGrouping": "clustered",
"lineSize": 3, "lineSmooth": true, "lineDataSymbolSize": 8,
"firstSliceAng": 0, "radarStyle": "standard",
"chartColors": ["00ACFA"], "chartColorsOpacity": 100,
"chartArea": { "fill": { "color": "FFFFFF" } },
"plotArea": { "fill": { "color": "FFFFFF" } },
"layout": { "x": 0, "y": 0, "w": 1, "h": 1 } } }
]
}
]
}
tip

Use Get Library Download URLs to hand the finished deck to someone as a link, or Send Email to attach it. To write a document rather than a deck, see Write DOCX From Markdown or Write PDF From Markdown.