This guide walks through a complete setup that pulls data from Airtable, populates a Google Doc template, and exports a PDF — all automated with Google Apps Script.
https://airtable.com/appXXXXXXXX/api/docs) and the Table name.Tip: In Airtable's API docs for your base, you can see the exact field names and example responses. Keep this page open — you'll need it.
Dear {{Name}},
Thank you for completing {{Course}} on {{Date}}.
Your score was {{Score}}.https://docs.google.com/document/d/DOCUMENT_ID_HERE/editmyFunction() code.Paste the following script and fill in your own values:
// ── CONFIG ──────────────────────────────────────────────
const AIRTABLE_TOKEN = 'patXXXXXXXXXXXXXX'; // Your Airtable Personal Access Token
const AIRTABLE_BASE_ID = 'appXXXXXXXXXXXXXX'; // Your Base ID
const AIRTABLE_TABLE = 'Clients'; // Your table name
const TEMPLATE_DOC_ID = '1aBcDeFgHiJkLmNoPqRsTuV'; // Your Google Doc template ID
const OUTPUT_FOLDER_ID = '1ZyXwVuTsRqPoNmLkJiHg'; // Google Drive folder ID for PDFs
// ────────────────────────────────────────────────────────
function generatePDFsFromAirtable() {
const records = fetchAirtableRecords();
const folder = DriveApp.getFolderById(OUTPUT_FOLDER_ID);
records.forEach(record => {
const fields = record.fields;
createPDF(fields, folder);
});
}
// Fetch all records from Airtable
function fetchAirtableRecords() {
const url = `https://api.airtable.com/v0/${AIRTABLE_BASE_ID}/${encodeURIComponent(AIRTABLE_TABLE)}`;
const response = UrlFetchApp.fetch(url, {
headers: { Authorization: `Bearer ${AIRTABLE_TOKEN}` }
});
const data = JSON.parse(response.getContentText());
return data.records;
}
// Copy template, replace placeholders, export as PDF
function createPDF(fields, folder) {
// Copy the template Doc
const templateFile = DriveApp.getFileById(TEMPLATE_DOC_ID);
const fileName = `${fields['Name'] || 'Document'} - ${fields['Date'] || ''}`;
const copy = templateFile.makeCopy(fileName, folder);
const doc = DocumentApp.openById(copy.getId());
const body = doc.getBody();
// Replace each placeholder with the Airtable field value
Object.keys(fields).forEach(key => {
const value = fields[key] !== undefined ? String(fields[key]) : '';
body.replaceText(`{{${key}}}`, value);
});
doc.saveAndClose();
// Export as PDF and save to the same folder
const pdfBlob = copy.getAs('application/pdf').setName(`${fileName}.pdf`);
folder.createFile(pdfBlob);
// Delete the temporary Google Doc copy
copy.setTrashed(true);
}https://drive.google.com/drive/folders/FOLDER_ID_HEREOUTPUT_FOLDER_ID in the script.Add a filterByFormula parameter to only process certain rows (e.g. only records marked "Ready"):
const url = `https://api.airtable.com/v0/${AIRTABLE_BASE_ID}/${encodeURIComponent(AIRTABLE_TABLE)}?filterByFormula=${encodeURIComponent("{Status}='Ready'")}`;Add this inside createPDF() after the PDF is created:
if (fields['Email']) {
GmailApp.sendEmail(
fields['Email'],
'Your document is ready',
'Please find your document attached.',
{ attachments: [pdfBlob] }
);
}generatePDFsFromAirtable → Time-driven → e.g. every day at 8am.After creating the PDF file, you can patch the record back:
function updateAirtableRecord(recordId, pdfUrl) {
const url = `https://api.airtable.com/v0/${AIRTABLE_BASE_ID}/${encodeURIComponent(AIRTABLE_TABLE)}/${recordId}`;
UrlFetchApp.fetch(url, {
method: 'patch',
contentType: 'application/json',
headers: { Authorization: `Bearer ${AIRTABLE_TOKEN}` },
payload: JSON.stringify({ fields: { 'PDF Link': pdfUrl } })
});
}| Problem | Fix |
|---|---|
| "Exception: Request failed for api.airtable.com" | Check your token and Base ID. Make sure the token has read access to the base. |
| Placeholders not replaced | Double-check field names in Airtable match exactly — they're case-sensitive. |
| PDF is blank or garbled | Make sure doc.saveAndClose() runs before the PDF export. |
| Script times out | Airtable returns max 100 records per page. Add pagination using the offset parameter for larger tables. |
| Authorisation error | Re-run the script and accept all Google permission prompts. |
Airtable paginates at 100 records. Replace fetchAirtableRecords() with:
function fetchAirtableRecords() {
let records = [];
let offset = null;
do {
const url = `https://api.airtable.com/v0/${AIRTABLE_BASE_ID}/${encodeURIComponent(AIRTABLE_TABLE)}`
+ (offset ? `?offset=${offset}` : '');
const response = UrlFetchApp.fetch(url, {
headers: { Authorization: `Bearer ${AIRTABLE_TOKEN}` }
});
const data = JSON.parse(response.getContentText());
records = records.concat(data.records);
offset = data.offset || null;
} while (offset);
return records;
}| Component | Purpose |
|---|---|
| Airtable | Data source (records + API) |
| Google Doc | Visual template with {{placeholders}} |
| Apps Script | Glue — fetches, fills, exports |
| Google Drive | PDF storage |