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
- Node interface
- Quick start
- The presentation definition
- Coordinates, layouts and the slide grid
- Slides
- Element reference
- Styling: defaults, roles and the merge order
- Images and the assets array
- Limits
- Examples
- Generating decks programmatically
- Troubleshooting
- Full JSON shape cheat sheet
1. Node interface
Inputs
| Port | Kind | Required | Description |
|---|---|---|---|
run | Event | yes | Executes the node. |
data | Data | yes | The presentation definition. Either an object, or a JSON string that parses to one. Arrays are rejected. |
name | Data / text field | yes | Output file name. .pptx is appended automatically if you leave it off. Must be a non-empty string. |
path | Data / text field | no | Library 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
| Port | Kind | Description |
|---|---|---|
done | Event | Fires after the node runs — including when it fails. |
file | Data | On 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
}
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.error | Cause |
|---|---|
No project ID found | The workflow has no runtime project — an environment problem, not a data problem. |
No data provided | data was null/undefined. |
Data is a string but not valid JSON | data was a string that failed JSON.parse. |
Data must be a presentation definition object | data was an array, a number, etc. |
Invalid file name | name 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 buffer | Renderer produced nothing (internal). |
Error uploading PPTX file to library | Render 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.
| Key | Type | Required | Notes |
|---|---|---|---|
slides | array | yes | 1–250 slides. |
version | integer | no | If present it must be exactly 1. |
layout | string | no | LAYOUT_WIDE (default), LAYOUT_16x9, LAYOUT_16x10, LAYOUT_4x3. |
metadata | object | no | title, subject, author, company, revision (integer ≥ 1). |
theme | object | no | headFontFace (default Aptos Display), bodyFontFace (default Aptos), lang (default en-GB). |
defaults | object | no | Per-element-type default styles plus named text roles. See §7. |
assets | array | no | Declares 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
- Colours are 6 hex digits with no
#."1F2937"✅ —"#1F2937"❌,"fff"❌,"red"❌. This applies everywhere a colour appears. - Strict objects. Every object in the schema is closed. A typo like
fontsizeorcolour, 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 (0is rejected; use0.01).- All four are required on every element.
x + wandy + hmust fit inside the slide, or you getElement extends beyond the slide boundary.
Slide sizes
layout | Width | Height | Use for |
|---|---|---|---|
LAYOUT_WIDE (default) | 13.333" | 7.5" | Modern 16:9 decks. |
LAYOUT_16x9 | 10" | 5.625" | 16:9 on the older 10" canvas. |
LAYOUT_16x10 | 10" | 6.25" | 16:10. |
LAYOUT_4x3 | 10" | 7.5" | Legacy projectors. |
A grid that works on LAYOUT_WIDE
Copy these numbers and your decks will look consistent:
| Region | x | y | w | h |
|---|---|---|---|---|
| Safe margin | 0.6 | 0.5 | 12.13 | 6.5 |
| Title | 0.6 | 0.55 | 12.13 | 0.9 |
| Subtitle / kicker | 0.6 | 1.45 | 12.13 | 0.5 |
| Body (full width) | 0.6 | 2.1 | 12.13 | 4.6 |
| Left column (of 2) | 0.6 | 2.1 | 5.87 | 4.6 |
| Right column (of 2) | 6.87 | 2.1 | 5.87 | 4.6 |
| Three columns | 0.6 / 4.84 / 9.09 | 2.1 | 3.84 | 4.6 |
| Footer | 0.6 | 6.85 | 12.13 | 0.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)
}
| Key | Type | Notes |
|---|---|---|
elements | array | Required. Up to 500 per slide. Rendered in array order — later elements draw on top. |
id | string | Optional label for your own tooling. Has no effect on output. |
background | object | { color, transparency? }. color required if background is present. |
notes | string | string[] | Speaker notes. An array is joined with a blank line between entries. |
master | string | ⚠️ Not usable from this node — see below. |
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.
type | Renders | Required keys |
|---|---|---|
text | A text box | position, and exactly one of text / runs |
image | An embedded picture | assetId, position |
shape | Any PptxGenJS shape, optionally with a label | shape, position |
line | A straight line / connector | position |
table | A table | rows, position |
chart | A native PowerPoint chart | chartType, 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
| Key | Type | Notes |
|---|---|---|
fontFace | string | e.g. "Aptos", "Arial". Font must exist on the viewer's machine. |
fontSize | number 1–400 | Points. |
bold, italic, underline, strike | boolean | |
color | hex6 | Text colour. |
highlight | hex6 | Highlighter colour behind the glyphs. |
align | left | center | right | justify | Horizontal. |
valign | top | middle | bottom | Vertical, within the box. Defaults to middle — set "top" for body copy in a tall box. |
margin | number 0–5, or [l, t, r, b] | Inset in points. 0 for pixel-tight layouts. |
breakLine | boolean | On a run: end the paragraph after it. |
charSpacing | number −20–200 | Tracking, in points. |
paraSpaceBeforePt / paraSpaceAfterPt | number 0–400 | ⚠️ No effect — see known no-ops. Use lineSpacingMultiple or empty runs instead. |
lineSpacingMultiple | number 0.1–10 | 1.2 for comfortable body text. |
isTextBox | boolean | Treat as a text box (enables autofit behaviour). |
fit | none | shrink | resize | shrink = shrink text to fit the box. |
transparency | number 0–100 | Percent. |
rotate | number 0–359 | Degrees clockwise. |
fill | { color, transparency } | Box background. |
line | line object (below) | Box border. |
shadow | shadow object (below) | |
hyperlink | { url } or { slide } + optional tooltip | See below. |
bullet | true, or bullet object (below) | Turns the paragraph into a list item. |
Bullet object
| Key | Type | Notes |
|---|---|---|
type | bullet | number | number gives an ordered list. |
code | string | Unicode hex for a custom glyph, e.g. "25BA" (▶), "2713" (✓), "2716" (✖). Must be exactly 4 hex digits or it silently falls back to •. |
style | string | Numbering style: arabicPeriod (default), arabicParenR, alphaLcPeriod, alphaUcPeriod, romanLcPeriod, romanUcPeriod. |
startAt | number ≥ 1 | The number this list item shows. See the numbering note below. |
indent | number 0–500 | Gap between the bullet glyph and the text, in points. Default ≈ 27pt. |
hanging | number 0–500 | ⚠️ No effect — see known no-ops. |
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 }.
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:
| Key | Type | Notes |
|---|---|---|
url | string | Must start with https://. http:// and mailto: are rejected. Max 2048 chars. |
slide | number 1–10000 | Jump to a slide number inside this deck. |
tooltip | string | Optional, max 500 chars, combines with either. |
Line object (used by style.line, shapes, lines, table borders)
| Key | Type | Notes |
|---|---|---|
color | hex6 | |
width | number 0–100 | Points. |
transparency | number 0–100 | |
dash | enum | solid, dash, dashDot, dot, lgDash, lgDashDot, lgDashDotDot, sysDash, sysDashDot, sysDashDotDot, sysDot |
beginArrowType / endArrowType | string | none, arrow, triangle, stealth, diamond, oval |
Shadow object
| Key | Type | Notes |
|---|---|---|
type | outer | inner | none | Required when shadow is present. |
color | hex6 | |
opacity | number 0–1 | Note: 0–1, not 0–100. |
blur | number 0–100 | Points. |
angle | number 0–359 | Degrees. |
distance | number 0–100 | Points. |
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 key | Type | Notes |
|---|---|---|
fit | contain (default) | cover | stretch | See below. |
altText | string | Accessibility description, max 2000 chars. |
rounding | boolean | true crops the image to a circle. |
transparency | number 0–100 | |
rotate | number 0–359 | |
shadow | shadow object | |
hyperlink | { url } or { slide } | Same rules as text hyperlinks. |
Fit modes
| Mode | Behaviour |
|---|---|
contain | Whole image fits inside w × h, aspect ratio kept, letterboxed. Safest default. |
cover | Image fills w × h, aspect ratio kept, overflow cropped. Good for hero/banner images. |
stretch | Image 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" }
}
| Key | Type | Notes |
|---|---|---|
shape | string | A PptxGenJS shape name — see the list below. Unknown names give Unsupported PptxGenJS shape: <name>. |
style | shape style | fill, line, shadow, transparency, rotate, flipH, flipV. |
text | string | Optional label drawn in the same box. |
textStyle | text style | Styling for that label. |
Notes:
- The label is drawn as a separate text box occupying the same rectangle, so
valign: "middle"andalign: "center"are how you centre it. roleis not available on shape labels — settextStyleexplicitly (or lean ondefaults.text).style.radiusis accepted by the schema but ignored. For rounded corners use theroundRectshape.
Common shape names (179 are available; these are the ones you'll actually reach for):
| Purpose | Names |
|---|---|
| Boxes | rect, roundRect, snip1Rect, snip2DiagRect, round2SameRect, bevel, frame, halfFrame, plaque |
| Basic geometry | ellipse, 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 |
| Arrows | rightArrow, leftArrow, upArrow, downArrow, leftRightArrow, upDownArrow, quadArrow, bentArrow, uturnArrow, curvedRightArrow, curvedLeftArrow, curvedUpArrow, curvedDownArrow, notchedRightArrow, stripedRightArrow, swooshArrow, circularArrow |
| Process / banners | chevron, homePlate, ribbon, ribbon2, wave, doubleWave, verticalScroll, horizontalScroll, funnel, gear6, gear9 |
| Callouts | wedgeRectCallout, wedgeRoundRectCallout, wedgeEllipseCallout, cloudCallout, borderCallout1, accentCallout1, rightArrowCallout, leftArrowCallout, upArrowCallout, downArrowCallout |
| Flowchart | flowChartProcess, flowChartDecision, flowChartTerminator, flowChartAlternateProcess, flowChartDocument, flowChartMultidocument, flowChartPredefinedProcess, flowChartInputOutput, flowChartPreparation, flowChartManualInput, flowChartManualOperation, flowChartConnector, flowChartOffpageConnector, flowChartMerge, flowChartSort, flowChartDelay, flowChartDisplay, flowChartMagneticDisk, flowChartInternalStorage |
| Stars & maths | star4, star5, star6, star7, star8, star10, star12, star16, star24, star32, mathPlus, mathMinus, mathMultiply, mathDivide, mathEqual, mathNotEqual |
| Braces & misc | leftBrace, 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 } }
}
styleis the shape style object (fill,line,shadow,transparency,rotate,flipH,flipV) — the stroke itself lives instyle.line.- Because
wandhmust both be> 0, a perfectly horizontal rule usesh: 0.01, and a perfectly vertical one usesw: 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:
| Form | Example | Notes |
|---|---|---|
| string | "EMEA" | |
| number | 1480 | Converted to text. |
| boolean | true | Converted to "true" / "false". |
null | null | Renders 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:
| Key | Type | Notes |
|---|---|---|
colW | number, or number[] | Column widths in inches. Array length should match your column count. |
rowH | number, or number[] | Row heights in inches. |
border | line object, or [top, right, bottom, left] | A 4-array styles each edge separately. |
autoFit | boolean | Fit the table to the given w. |
autoPage | boolean | Overflow onto new slides automatically. |
autoPageRepeatHeader | boolean | Repeat header rows on each continuation slide. |
autoPageHeaderRows | integer 0–100 | How many rows count as the header. |
autoPageLineWeight / autoPageCharWeight | number −10–10 | Tune 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:
| Key | Type | Notes |
|---|---|---|
name | string | Legend label. |
labels | string[] | Category labels. labels.length must equal values.length. |
values | number[] | Data points. |
sizes | number[] | Optional, bubble charts only. Must match values.length. |
Chart style (all optional):
| Group | Keys |
|---|---|
| Title | showTitle, title, titleAlign, titleBold, titleColor, titleFontFace, titleFontSize |
| Legend | showLegend, legendPos (b/l/r/t/tr), legendColor, legendFontFace, legendFontSize |
| Data labels | showValue, showPercent, showSerName, showLabel, showLeaderLines, dataLabelColor, dataLabelFontBold, dataLabelFontFace, dataLabelFontItalic, dataLabelFontSize, dataLabelFormatCode, dataLabelPosition (b/bestFit/ctr/l/r/t/inEnd/outEnd) |
| Axes | catAxisLabelColor, catAxisLabelFontFace, catAxisLabelFontSize, catAxisHidden, valAxisLabelColor, valAxisLabelFontFace, valAxisLabelFontSize, valAxisHidden |
| Bar | barDir (col = vertical, bar = horizontal), barGrouping (clustered, stacked, percentStacked, standard) |
| Line | lineSize, lineSmooth, lineDataSymbolSize |
| Pie / doughnut | firstSliceAng |
| Radar | radarStyle (standard, marker, filled) |
| Colour | chartColors (hex6[]), chartColorsOpacity, invertedColors |
| Frame | chartArea / 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.
chartColorsmaps to slices in order.showPercent: trueis usually what you want. - bar / bar3d —
barDir: "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", ... }].labelsare 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/chartapply to every element of that type.defaults.roles.<name>is text-only. Atextelement opts in with"role": "<name>".- Role names must match
^[A-Za-z0-9_-]{1,100}$; up to 100 roles.
Merge order (later wins)
| Element | Order |
|---|---|
text | defaults.text → defaults.roles[role] → position → element style |
image | defaults.image → element style (then position) |
shape | defaults.shape → position → element style (label: defaults.text → position → textStyle) |
line | defaults.line → position → element style |
table | defaults.table → position → element style |
chart | defaults.chart → position → 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:
| Key | Where | Workaround |
|---|---|---|
paraSpaceBeforePt, paraSpaceAfterPt | text style, run options | Use lineSpacingMultiple, or insert an empty run ({"text":" ","options":{"fontSize":8,"breakLine":true}}) between paragraphs. |
bullet.hanging | text style, run options | Use bullet.indent. |
radius | shape style, line style | Use the roundRect shape. |
id | any element, any slide | Purely a label for your own tooling. |
master | slide | Not 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" }
]
| Key | Meaning |
|---|---|
assetId | The handle your image elements use. Any string you like. Must be unique. |
id | The 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/pngMIME givesMIME 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
fileoutput 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>.
| Limit | Default | Hard max |
|---|---|---|
| Input JSON size | 2 MB | 10 MB |
| Output file size | 100 MB | 250 MB |
| Slides | 100 | 250 |
| Elements per slide | 250 | 500 |
| Total elements | 5,000 | 20,000 |
| Text chars per element | 100,000 | 250,000 |
| Total text chars | 1,000,000 | 5,000,000 |
| Table rows | 1,000 | 5,000 |
| Table columns | 50 | 100 |
| Total table cells | 20,000 | 100,000 |
| Chart series | 20 | 100 |
| Chart points per series | 5,000 | 20,000 |
| Image bytes | 20 MB | 50 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 } }
]
}
]
}
]
}
10.4 Rich text: mixed runs, highlight, links
{
"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 }
}
]
}
]
}
10.12 Images: hero banner and a repeated logo
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)returningM + i * (w + gutter)withw = (13.333 − 2M − (n−1) × gutter) / nremoves every arithmetic slip in multi-column slides. - A
bullets(items, opts)function that maps items to runs withbreakLine: trueand, 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.333ory + h > 7.5; a failure you can see instderrbeatsElement extends beyond the slide boundary.from the renderer.
Using AI to write the deck
Two approaches, and the second is much more reliable:
- 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.
- 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.
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:
| Check | Fix |
|---|---|
A colour with a #, 3 digits, or a name | Use 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 neither | Supply exactly one. |
w or h of 0 | Must be greater than 0 — use 0.01 for rules and hairlines. |
A missing x, y, w or h | All four are required on every element. |
version set to something other than 1 | Either omit it or set 1. |
A slide missing elements | Required, though it may be []. |
shadow without type | type 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 digits | Use e.g. "2713"; anything else silently falls back to •. |
| A PptxGenJS option copied from elsewhere | If 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
| Symptom | Cause | Fix |
|---|---|---|
| Body text sits in the vertical middle of its box | valign defaults to middle | Set "valign": "top". |
| Text spills out of its box | No 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-increment | Set startAt on every item — startAt: i + 1. |
A custom bullet shows as • | code wasn't exactly 4 hex digits | Use the 4-digit form, e.g. "25BA". |
| Sub-bullets don't indent | There are no outline levels | Use a second text box shifted right, or a different glyph. |
| Paragraph spacing has no effect | paraSpaceBeforePt / paraSpaceAfterPt are no-ops | Use lineSpacingMultiple, or an empty run between paragraphs. |
A rect has square corners despite radius | radius is ignored | Use the roundRect shape. |
| A line is invisible | style.line.width is 0, or the stroke was put outside style.line | Put colour and width in style.line. |
| A shape label isn't centred | The label is its own text box over the same rectangle | Set align: "center" and valign: "middle" in textStyle. |
| A rotated shape's label isn't rotated | rotate on the shape doesn't rotate the label | Set rotate in textStyle too. |
| Fonts look wrong when someone else opens it | Fonts aren't embedded | Stick to fonts your audience has, e.g. Aptos, Arial, Calibri. |
| An element is hidden behind another | Later elements draw on top | Reorder the elements array. |
Bounds, images, charts and limits
| Error | What 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 gif | The 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 mislabelled | labels.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
| Error | Meaning |
|---|---|
No data provided | The data input never received a value — check the wire and that the upstream node actually ran. |
Data is a string but not valid JSON | A string arrived that JSON.parse rejected, often a truncated LLM response or a double-encoded payload. |
Data must be a presentation definition object | An array arrived. Wrap it: { "slides": [...] }. |
Invalid file name | name is blank. It has no default. |
No project ID found | The workflow has no runtime project — not a data problem. |
Error uploading PPTX file to library | The file rendered but storage rejected the write. Retry; check the library path. |
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 } } }
]
}
]
}
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.