# Generate PDFs from Airtable records Generate PDFs from Airtable records with an automation script that renders HTML to PDF and writes the link back, plus the limits of the native options. Airtable is often the database behind quotes, invoices, certificates and work orders. Turning a record into a polished PDF is a frequent need, and Airtable's built-in tools only get you part of the way. **Quick answer:** in an Airtable automation, add a **Run a script** action that posts the record's HTML to the API and saves the returned link. ```js const res = await fetch("https://html-pdf.com/v1/pdf", { method: "POST", headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json" }, body: JSON.stringify({ html, response: "url" }), }); ``` The full script is in Step 3 below. ## Native options and their limits This section covers what Airtable can do on its own, and where it stops. - **Page designer extension.** Lets you lay out fields visually and print or save as PDF. It's manual. Someone opens the extension and prints, one record or a batch at a time, and you can't trigger it from an automation. - **Printing a record or view.** Fine for internal use, not for branded documents. - **Marketplace extensions and integrations.** Some document-generation services connect to Airtable, usually with their own template editors and pricing. If you want PDFs created automatically when a record changes, with your own HTML and CSS, use an automation script and an HTML-to-PDF API. ## How the automation works Here is the whole flow in four steps. 1. A trigger fires (record enters a view, a checkbox is ticked, a status changes). 2. A **Run a script** action builds HTML from the record's fields. 3. The script sends it to `POST https://html-pdf.com/v1/pdf` with `"response": "url"`. 4. The API returns a link to the PDF, and the script writes it into a URL field (or an attachment field) on the record. Automation scripts run on Airtable's servers and can call `fetch` directly. You only need `remoteFetchAsync` in the Scripting extension. Each run has limits on execution time and the number of fetch requests (Airtable documents 50 per run). Process one record per run. ## Step 1: prepare the table Start by adding two fields to hold the result. Say you have an `Invoices` table with `Number`, `Customer`, `Amount` and `Status`. Add these two fields. - `PDF` (URL field) for the link. - `PDF file` (Attachment field, optional) if you want Airtable to keep a copy. Get an API key at https://html-pdf.com/login (100 free credits, 1 credit = 1 PDF page). ## Step 2: create the automation Next, set up the trigger and the script action. 1. **Automations → Create automation.** 2. Trigger: **When record matches conditions** (for example `Status` is `Ready`) or **When record enters view**. 3. Action: **Run a script**. 4. In the script's left panel, add an input variable `recordId` set to the trigger record's **Airtable record ID**. The script below keeps the API key in a constant. Anyone who can edit the base's automations can see it, so use a dedicated key and rotate it if collaborators change. ## Step 3: the script Paste this script into the action and replace the API key. ```js const { recordId } = input.config(); const apiKey = "YOUR_API_KEY"; const table = base.getTable("Invoices"); const record = await table.selectRecordAsync(recordId); if (!record) throw new Error("Record not found"); const esc = (v) => String(v ?? "").replace(/[&<>"]/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """ }[c])); const number = record.getCellValueAsString("Number"); const customer = record.getCellValueAsString("Customer"); const amount = Number(record.getCellValue("Amount") || 0); const html = `
Bill to: ${esc(customer)}
Total: $${amount.toFixed(2)}
`; const res = await fetch("https://html-pdf.com/v1/pdf", { method: "POST", headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json", }, body: JSON.stringify({ html, prefer_css_page_size: true, print_background: true, filename: `invoice-${number}.pdf`, response: "url", }), }); const data = await res.json(); if (!res.ok) throw new Error(`${data.error.code}: ${data.error.message}`); await table.updateRecordAsync(recordId, { "PDF": data.url, // Optional: let Airtable download and keep its own copy "PDF file": [{ url: data.url, filename: `invoice-${number}.pdf` }], }); output.set("pdfUrl", data.url); ``` `getCellValueAsString` handles linked records, lookups and single selects, so you can pull data from related tables without extra queries. ### About the 24-hour link With `"response": "url"` the link is valid for 24 hours. For a permanent copy, write it to an **attachment field** as shown. Airtable fetches the file from the URL and stores it. If you only need to email the PDF right away, the URL field is enough. Add a **Send email** action after the script and use the `pdfUrl` output. ## Line items from linked records This snippet turns linked line items into table rows for your invoice. Read the linked IDs from the `Line items` field, query that table and build the rows. ```js const links = record.getCellValue("Line items") || []; const itemsTable = base.getTable("Line items"); const items = await itemsTable.selectRecordsAsync({ recordIds: links.map((l) => l.id), fields: ["Name", "Qty", "Price"] }); const rows = items.records.map((r) => `