PaperType

Generate a PDF from an Airtable record and attach it to the record

Airtable stores the data but cannot turn one record into a formatted document. Its Run a script automation action can call an external API with fetch (Airtable's help page says exactly that), so one script posts the record's fields to the API, gets a 24-hour link to the finished PDF back, and writes that link into an attachment field; Airtable downloads the file and keeps its own copy. This page gives you the script (papertype-airtable.js, about 100 lines, MIT) with two modes: a Field | Value report of any record, and an invoice from the hosted template. The script was executed end to end on 2026-10-23 with only Airtable's objects stubbed; it was not run inside an Airtable base, so the Airtable UI steps are quoted from Airtable's documentation fetched the same day.

Setup: attachment field, trigger, script input

Create a free API key at /signup. In the base:

  • Add an attachment field to the table (the script calls it PDF; change ATTACHMENT_FIELD if yours differs) and a checkbox such as Make PDF.
  • Create an automation with the trigger When record matches conditions: table = yours, condition Make PDF is checked. Airtable's help page for this trigger says it fires when a record changes into the matching state, not for records already matching, and recommends triggering on a single field that changes once (a checkbox someone ticks when the record is ready) rather than on several fields, which can fire partway through someone's typing.
  • Add the action Run a script and paste the script. In the action's Script input panel click + Add property for each key (Airtable's scripting reference: keys are string literals, values may be template expressions that refer to earlier steps): recordId → the trigger step's Record ID; apiKey → your dp_… key; mode → report or invoice.
  • Edit the three settings at the top of the script: TABLE, ATTACHMENT_FIELD and, for report mode, FIELDS (the fields to include, in order; the primary field becomes the title). Test the step from the automation editor, then switch the automation on.

Where the key lives: in the automation's configuration, which Airtable's help page says owners and creators can edit and editors can view. Use a key created for this base only, and revoke it from your account page if the base is shared more widely later. The script reads it with input.config() and sends it only to https://papertype.dev/v1/pdf.

const cfg = input.config();            // {recordId, apiKey, mode}
const table = base.getTable(TABLE);
const record = await table.selectRecordAsync(cfg.recordId);
const values = {};
for (const n of FIELDS) values[n] = record.getCellValueAsString(n);
const file = await renderPdf(reportBody(record.name, values), cfg.apiKey);  // {url, filename, pages, bytes, expires_at}
const existing = record.getCellValue(ATTACHMENT_FIELD) || [];
await table.updateRecordAsync(record, { [ATTACHMENT_FIELD]: [...existing, { url: file.url, filename: file.filename }] });

Two details come straight from Airtable's scripting reference: a new attachment needs only url (plus an optional filename), and writing an attachment cell replaces it, so the script spreads the existing attachments in front of the new one. record.name is the primary field as a string. output.set('pdfUrl', …) and output.set('pages', …) make the link and page count available to later steps, for example a Send email action.

Report mode: record → Field | Value PDF

For a record whose primary field is Q4 launch plan and whose fields are Owner · Due · Status · Notes = Jane Doe · 2026-11-15 · On track | review Friday · Line one⏎Line two, the script sends:

{
  "markdown": "# Q4 launch plan\n\n| Field | Value |\n|---|---|\n| Owner | Jane Doe |\n| Due | 2026-11-15 |\n| Status | On track \\| review Friday |\n| Notes | Line one<br>Line two |",
  "title": "Q4 launch plan",
  "theme": "clean",
  "page_numbers": true,
  "filename": "Q4-launch-plan.pdf",
  "output": "url"
}

"output": "url" is what makes this work without a server: instead of PDF bytes the API returns JSON, {"url": "https://papertype.dev/v1/files/…pdf", "filename": "Q4-launch-plan.pdf", "pages": 1, "bytes": …, "expires_at": …}, and the link serves the PDF to anyone who has it for 24 hours, no key needed, which is how Airtable's attachment fetcher gets it. Live result on 2026-10-23: 200, one page; the link, fetched with a plain GET and no headers, returned application/pdf. Pipes in a value are escaped and line breaks become <br>, so a long-text field renders inside one table cell. Dates, numbers, selects and linked records arrive as the string Airtable shows in the cell (getCellValueAsString).

Invoice mode with the hosted template

With mode = invoice the script reads eight fields by name, Number, Date, Due date, Client, Client address (long text, one line per address line), Items (long text, one line per item: description, quantity, unit price), Tax rate and Currency, and fills the hosted invoice template. Edit the from block in the script once with your own details. The record INV-1042 · 2026-10-23 · 2026-11-22 · Acme Client Ltd · 1 Sample Street⏎Shelbyville · Design, October, 10, 120⏎Hosting, 1, 45.50 · 8.25 becomes:

{
  "template": "invoice",
  "data": {
    "number": "INV-1042", "date": "2026-10-23", "due_date": "2026-11-22", "currency": "USD",
    "from": {"name": "Northwind Studio", "address_lines": ["12 Example Road", "Springfield"]},
    "to": {"name": "Acme Client Ltd", "address_lines": ["1 Sample Street", "Shelbyville"]},
    "items": [
      {"description": "Design, October", "quantity": 10, "unit_price": 120},
      {"description": "Hosting", "quantity": 1, "unit_price": 45.5}
    ],
    "tax_rate": 8.25, "tax_label": "Sales tax"
  },
  "page_size": "Letter",
  "filename": "INV-1042.pdf",
  "output": "url"
}

Live result: one Letter page, attached as INV-1042.pdf, totals $1,200.00 + $45.50 + 8.25% tax = $1,348.25. A record with an empty Number makes the script fail with PaperType 422: Missing required field(s) for template 'invoice': number, which appears in the automation's run history; nothing is written back to the record. The certificate and report templates take the same route with their own fields.

Limits that matter in Airtable automations

  • Airtable's script limits (its Run a script help page, fetched 2026-10-23): script timeout 30 seconds, "temporarily adjusted to 120 seconds"; fetch times out after 30 seconds; 50 fetch requests and 15 mutations per second per run; 512 MB memory. A render takes well under a second, so one record is one fetch and one mutation. The same page says the action is not available on a Team plan trial.
  • Response size. Airtable's scripting reference caps a fetch response at 4.5 MB. The script never downloads the PDF (it receives a short JSON body and hands Airtable the link), so the PDF's size does not count against this.
  • The 24-hour link. Airtable fetches the file when the record is updated and serves it from its own storage afterwards (the cell's read format returns airtableusercontent.com URLs), so our link only has to live through that download. If Airtable cannot fetch a URL, its API reports attachmentsFailedUploading in the update response; re-run the automation and a fresh link is made.
  • Our quota. Free-plan output carries a small papertype.dev footer mark. Paid plans (from $9/month) do not. The free plan is 50 documents a month; the API returns 429 at the limit, never overage. A base that produces 200 PDFs a month needs the $9 plan. Failed renders are not counted.
  • Images. The renderer has no network access. Images, fonts and stylesheets at external URLs are never fetched; inline them as data: URIs. A request that references an external URL fails with a 422 that names the URL, rather than silently dropping the asset. Airtable's own attachment URLs also expire after two hours (its field reference), so for a logo, fetch the attachment inside the script, base64-encode it and put a data: URI into the template's logo field. This path was not exercised in the test.
  • 2 MB of source per request, 300 pages per document, 2 concurrent renders per account. One record is far below 2 MB.
  • Errors. 401 (key missing or wrong), 413, 422 (named in the message) and 429 come back as JSON with a detail field; the script throws them as PaperType <status>: … so the run history shows the cause.

Without automations: the Web API from your own code

If a server or a scheduled job already produces the PDF, attach it with Airtable's REST API instead of a script. Render with "output": "url" as above, then (from Airtable's Update record reference) send a PATCH with the attachment field set to the link; PATCH updates only the fields you name, PUT clears the rest:

curl -X PATCH "https://api.airtable.com/v0/{baseId}/{tableIdOrName}/{recordId}" \
  -H "Authorization: Bearer $AIRTABLE_TOKEN" -H "Content-Type: application/json" \
  --data '{"fields": {"PDF": [{"url": "https://papertype.dev/v1/files/…pdf", "filename": "Q4-launch-plan.pdf"}]}}'

The token is a personal access token from airtable.com/create/tokens with the scope data.records:write and the base added to it (Airtable's authentication page: the token must have both the scope and the base, and the user needs editor access). Airtable's rate limit is 5 requests per second per base; over it you get 429 and must wait 30 seconds. Airtable also has an Upload attachment endpoint that takes base64 bytes up to 5 MB; its reference points to the URL method for anything larger, and the URL method needs no upload step at all. This section's Airtable request was not executed (it needs an Airtable account); the PaperType half of it was.

Questions

Did you test this inside Airtable?

Not inside a base. The script file itself was executed on 2026-10-23 with `input`, `base`, `record` and `output` replaced by stubs and everything else real: it built both request bodies, posted them to the live API with its own `fetch` call, received the JSON links (one page each), and wrote `{url, filename}` into the stubbed attachment field keeping an existing attachment in place; the links were then downloaded with a plain GET and checked to be PDFs. The 422 (blank invoice number) and 401 (bad key) paths were exercised and wrote nothing back. The trigger, script-input and attachment-fetch behaviour is quoted from Airtable's help and reference pages. If a step is wrong, tell support@papertype.dev and the page gets fixed.

Who can see my API key?

Anyone who can open the automation's configuration (owners and creators edit it, editors can view it, per Airtable's help page). Create a key just for this base and revoke it from your account page if that circle grows. The key is sent only to papertype.dev.

What happens when the 24-hour link expires?

Nothing, for the record: Airtable has already downloaded the file into the attachment field and serves it from its own storage. The expiry only limits how long the raw link works; the finished PDF is kept at that link for 24 hours and then deleted, and the record's source text is never stored.

Can I generate PDFs for many records at once?

Yes, within Airtable's per-run limits (50 fetch requests, 15 mutations a second) and our plan limits (50 documents a month free, 500 on the $9 plan). Loop over `table.selectRecordsAsync()` in a scheduled automation or in the scripting extension, calling `renderPdf` and `updateRecordAsync` per record. For hundreds of records at a time use the Web API route from a server instead.

Can I run it from a button instead of a trigger?

The scripting extension also has `fetch` (Airtable's reference) and `input.recordAsync`, which a button field's "Run script" action pre-fills with the button's record. Replace `input.config()` with a record picker and store the key in the extension's settings; this variant was not tested.

Related

All guides

Free previews carry a watermark and are deleted after 24 hours. Free API output carries a footer mark. The renderer runs no JavaScript and never fetches external URLs. Privacy · Terms · Refunds