Skip to content

Integration

Formfeed for Airtable

An Airtable base is a spreadsheet with an API, which makes it a natural source for documents. Airtable’s Run a script action calls the API directly — it runs on Airtable’s servers, so there is no CORS to work around — and a base renders and files its own invoices, certificates and reports today.

No extension to install: an automation script does it today. A Formfeed extension for Airtable is planned, with no date yet.

Free plan with 100 units a month, no credit card

One document per record

Trigger on a record entering a view, on a condition or on a button, add the Run a script action, and hand it the record id. The step-by-step recipe, with line items from a linked table, is in the documentation.

Airtable automation → Run a scriptJavaScript
const { recordId } = input.config();
const API_KEY = 'ff_test_…';
const table = base.getTable('Invoices');
const record = await table.selectRecordAsync(recordId);
const number = record.getCellValueAsString('Number');

const response = await fetch('https://api-eu.formfeed.dev/v1/renders', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${API_KEY}`,
    'Content-Type': 'application/json',
    'Idempotency-Key': `airtable-${recordId}-${number}`,
  },
  body: JSON.stringify({
    template: 'invoice-de',
    filename: `invoice-${number}.pdf`,
    data: { invoice: { number }, customer: { name: record.getCellValueAsString('Customer') } },
  }),
});
const render = await response.json();
if (render.status !== 'succeeded') throw new Error(render.detail ?? `Render is ${render.status}`);

await table.updateRecordAsync(recordId, {
  Document: [{ url: render.download_url, filename: `invoice-${number}.pdf` }],
  'Render ID': render.id,
});

The document lands in the record

  • Airtable takes a URL and downloads the file itself, so the signed link goes straight into the attachment field and the record keeps Airtable’s own copy.
  • Keep the render id in a text field too: a signed link expires after seven days at the latest, and one call to GET /renders/{id} issues a fresh one for as long as the file is kept.
  • Your own identifiers ride along in meta and come back with every webhook, which is how an asynchronous render finds its record again.

A whole view in one batch

A script step may make 50 requests and none may take longer than 30 seconds, so a loop over a view is the wrong shape. One call to the batch endpoint queues up to 1,000 documents instead, optionally with one zip of them all. The batch reports each document as it finishes to an Airtable "When webhook received" automation, which writes it into its record; every delivery is one automation run on your Airtable plan. Long single documents take the same route: mode: async plus the webhook URL. The recipe has both scripts.

Worth knowing before you build it

  • Airtable automations have no secret store, so everyone who can edit the base can read the key. Give each base its own with the smallest scopes.
  • Build with a test key: those renders are not metered and the files are deleted after a day.
  • An idempotency key makes Airtable’s reruns harmless — for 24 hours, the second one returns the first document instead of rendering and charging again.
  • In the Scripting extension the code runs in the browser, where the API refuses a key that does not allow that origin. Use remoteFetchAsync there.

What the extension will add

Planned, with no date yet: mapping a table’s fields to a template in a panel, rendering the records you select and filling the attachment field — the script above without the script. Until it exists, the script is the way.

Or a spreadsheet batch

Export the view as CSV and use "Batch from spreadsheet" in the app: map the columns once and download the zip. Nothing is stored in the base that way.

More integrations

Start rendering today

The free plan includes 100 units a month and free test renders. Change plans when you need more.