This is a technical decision aid. Confirm the current extension contract and validator evidence before implementation.
The reliable data-to-PowerPoint pattern is read, transform, generate. Read rows from Airtable, Notion, PostgreSQL, Google Sheets, or any API. Transform those rows into a Runstamp JSON schema. Generate a native PPTX with editable charts. The data source changes; the document contract stays the same.
This matters for reporting products, internal dashboards, investor updates, board decks, customer QBRs, and sales operations packs. Those decks are not hand-authored presentations. They are structured data turned into a file. If you are new to the schema layer, start with generate PPTX from JSON; this article focuses on connecting that schema to real data sources.
Data to PowerPoint architecture
The pattern has three steps:
- Read records from a source system.
- Transform records into a Runstamp JSON schema.
- Generate the
.pptxfile.
Steps 2 and 3 are identical regardless of data source. Only Step 1 changes.
How do you read from Airtable?
According to Airtable's official npm package, "the Airtable API is your own RESTful API for your base. You will use your table names to address tables, column names to access data stored in those columns."
npm install airtable @runstamp/pptx
import Airtable from "airtable";
import { generate } from "@runstamp/pptx";
import { writeFileSync } from "node:fs";
// 1. READ from Airtable
const base = new Airtable({
apiKey: process.env.AIRTABLE_TOKEN,
}).base(process.env.AIRTABLE_BASE_ID);
const records = await base("Sales")
.select({ view: "Grid view" })
.all();
const data = records.map(r => ({
region: r.get("Region"),
revenue: r.get("Revenue"),
growth: r.get("Growth"),
}));
// 2. TRANSFORM to JSON schema
const schema = buildDeckSchema(data);
// 3. GENERATE
writeFileSync("report.pptx", await generate(schema));
How do you read from Notion?
Notion's @notionhq/client package queries databases and returns structured property objects. Each property (title, number, select, date) has a specific type that you extract from the API response.
npm install @notionhq/client @runstamp/pptx
import { Client } from "@notionhq/client";
import { generate } from "@runstamp/pptx";
import { writeFileSync } from "node:fs";
// 1. READ from Notion database
const notion = new Client({
auth: process.env.NOTION_TOKEN,
});
const response = await notion.databases.query({
database_id: process.env.NOTION_DATABASE_ID,
});
const data = response.results.map(page => ({
region: page.properties.Region.title[0].plain_text,
revenue: page.properties.Revenue.number,
growth: page.properties.Growth.number,
}));
// 2. TRANSFORM + 3. GENERATE (identical to Airtable)
const schema = buildDeckSchema(data);
writeFileSync("report.pptx", await generate(schema));
How do you read from PostgreSQL?
Direct database queries with the pg package. No API wrapper, no rate limits, fastest option for large datasets.
npm install pg @runstamp/pptx
import pg from "pg";
import { generate } from "@runstamp/pptx";
import { writeFileSync } from "node:fs";
// 1. READ from PostgreSQL
const client = new pg.Client(process.env.DATABASE_URL);
await client.connect();
const { rows } = await client.query(`
SELECT region, SUM(amount) as revenue,
ROUND(100.0 * (SUM(amount) - LAG(SUM(amount))
OVER (ORDER BY region)) / LAG(SUM(amount))
OVER (ORDER BY region), 1) as growth
FROM orders
WHERE created_at >= '2026-07-01'
GROUP BY region ORDER BY revenue DESC
`);
await client.end();
const data = rows.map(r => ({
region: r.region,
revenue: Number(r.revenue),
growth: Number(r.growth),
}));
// 2. TRANSFORM + 3. GENERATE (identical)
const schema = buildDeckSchema(data);
writeFileSync("report.pptx", await generate(schema));
The shared data-to-PPTX transform function
All three integrations use the same buildDeckSchema function. It is a pure function: data in, JSON schema out. No side effects, no data-source-specific logic.
export function buildDeckSchema(data) {
return {
slides: [
{ elements: [
{ type: "text", value: "Q3 2026 revenue report",
style: { fontSize: 36, bold: true } },
]},
{ elements: [
{ type: "text", value: "Revenue by region",
style: { fontSize: 24, bold: true } },
{ type: "chart", chartType: "bar",
data: {
categories: data.map(d => d.region),
series: [{
name: "Revenue",
values: data.map(d => d.revenue),
}]
}},
]},
{ elements: [
{ type: "text", value: "Breakdown",
style: { fontSize: 24, bold: true } },
{ type: "table",
headers: ["Region", "Revenue", "Growth"],
rows: data.map(d => [
d.region,
`$${d.revenue.toLocaleString()}`,
`${d.growth}%`,
]) },
]},
]
};
}
The data parameter is an array of objects with region, revenue, and growth properties. Whether those objects come from Airtable's record.get(), Notion's page.properties, PostgreSQL's rows, or Google Sheets, the schema does not care. This is the JSON layer pattern applied to data integration.
Why not generate slides directly from each source?
It is tempting to write one deck generator per source: one for Airtable, one for Notion, one for PostgreSQL, one for Google Sheets. That works until the second output format appears.
The schema boundary gives you reuse:
| Without a schema boundary | With a schema boundary |
|---|---|
| Airtable code knows slide layout | Airtable code only maps rows to data objects |
| Notion code duplicates chart logic | Chart logic lives in the shared schema |
| PDF/DOCX/XLSX require new pipelines | The same data model can feed other Runstamp formats |
| AI agents must infer deck structure | Agents can emit validated JSON |
For a one-off script, direct slide code is fine. For a product workflow, use the intermediate JSON contract.
Extend: add more data sources
The pattern works with any data source that returns structured data. Replace Step 1 with your API client:
- Google Sheets — see the dedicated Google Sheets to PowerPoint tutorial
- Supabase —
supabase.from("table").select()(see the Edge Functions tutorial) - Stripe —
stripe.invoices.list()(see the Stripe invoice tutorial) - REST API —
fetch("https://api.example.com/data") - CSV file — parse with
papaparse, transform rows - MongoDB —
collection.find().toArray()
The transform function stays the same. The generate call stays the same. Only the data retrieval changes. For batch generation at scale, wrap the pattern with p-limit, BullMQ, or your existing queue.
Keep the document workflow in your product.
Use the catalog to select a native action, prove it against a real artifact, then embed review and release evidence where your customer already works.

