Skip to content
PaperJSX

[FIELD NOTE]

Generate PPTX from Airtable, Notion, or PostgreSQL

One pattern, any data source: read from Airtable, Notion, or PostgreSQL, transform rows into a PaperJSX JSON schema, generate a PPTX with charts.

2026-07-237 min read

This is a technical decision aid. Check current package and hosted-access details before implementation.

One pattern, any data source. Read records from Airtable, Notion, or PostgreSQL. Transform rows into a PaperJSX JSON schema. Generate a PPTX with charts. The data source changes; the generation code doesn't. This article shows the same quarterly report deck built from three different data sources — proving the pattern is data-source-agnostic. For Google Sheets, see the dedicated tutorial.

Data source integration pattern · read → transform → generate Data source Airtable Notion PostgreSQL Google Sheets any API... read transform() rows → JSON schema JSON PaperJSX .pptx / .pdf with charts paperjsx.com — fig 1

The pattern has three steps: Read (fetch records from the data source), Transform (map records to a PaperJSX JSON schema), and Generate (produce the file). 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 @paperjsx/json-to-pptx
import Airtable from "airtable";
import { generate } from "@paperjsx/json-to-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 @paperjsx/json-to-pptx
import { Client } from "@notionhq/client";
import { generate } from "@paperjsx/json-to-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 @paperjsx/json-to-pptx
import pg from "pg";
import { generate } from "@paperjsx/json-to-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 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 doesn't care. This is the JSON layer pattern applied to data integration.

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 tutorial
  • Supabasesupabase.from("table").select() (see the Edge Functions tutorial)
  • Stripestripe.invoices.list() (see the Stripe invoice tutorial)
  • REST APIfetch("https://api.example.com/data")
  • CSV file — parse with papaparse, transform rows
  • MongoDBcollection.find().toArray()

The transform function stays the same. The generate call stays the same. Only the data retrieval changes. For batch generation at scale (10,000+ documents from a database), wrap the pattern with p-limit or BullMQ.

Keep the workflow in your product.

Use local engines or local MCP to prove the artifact. When release controls matter, discuss an approved hosted evaluation for a real delivery.