Logo
Apdf tutorials 7 min read

Prefill PDF Forms from Google Sheets with n8n

The data is already in a spreadsheet, the PDF form already exists — and someone still copies each row into the form by hand. The usual workarounds hurt too: rebuild the form as a Google Docs template and export it (so it's no longer the original form), or dig out every field's exact name in a separate inspector tool before you can map anything.

You don't need either. In this tutorial an n8n workflow fills the PDF form you already have the moment a row lands in Google Sheets, then files the PDF in Google Drive and emails it — with the Apdf node and a few clicks.

What you'll build
A climbing gym's sign-up sheet that prepares every new member's agreement on its own: a row is added, and seconds later the filled, locked PDF is in Drive and in the member's inbox: Row added: Maya Thompson → membership-agreement-Maya Thompson.pdf → Drive + Gmail
Free plan: 1,000 PDF operations a month, no card
1

Match the sheet columns to the form fields

The form is Fernwood Climbing's one-page membership agreement: text fields, a radio group for the plan, a dropdown for the home gym and two checkboxes. It sits at a public URL, for example https://files.fernwoodclimbing.example/forms/membership-agreement.pdf.

To fill a field you need its name. Install the Apdf n8n node (@apdf/n8n-nodes-apdf), add an Apdf node with Resource PDF and Operation Read Form, paste the form's address as File URL and run the step once. It lists every field with its name, type, label and options. For this form, that gives the sheet its columns:

Sheet column Field name Takes
Namemember_nametext
Date of birthdate_of_birthtext
Emailemailtext
Phonephonetext
Planmembership_planradio: Monthly, Annual, Student
Home gymhome_gymdropdown: Riverside, Old Town, Harbor Point
Start datestart_datetext
Belay certifiedbelay_certifiedcheckbox: TRUE or FALSE
Emergency contactemergency_contacttext
Emergency phoneemergency_phonetext
Newsletternewslettercheckbox: TRUE or FALSE

Create the sheet with these headers in the first row. Text fields take whatever the cell shows, so write dates the way they should appear on the form. The checkbox columns can be Google Sheets checkboxes: their TRUE and FALSE tick and untick the boxes.

Heads up: Radio and dropdown values have to match an option exactly — “annual” is not “Annual”, and a single mismatch rejects the whole fill. Turn the Plan and Home gym columns into Google Sheets dropdowns with exactly these options, so nobody can type a variant.
2

Start the workflow on every new row

  1. In a new n8n workflow, add the Google Sheets Trigger and connect your Google account.
  2. Pick the spreadsheet under Document and the tab under Sheet.
  3. Set Trigger On to Row Added. Add a test row and fetch it, so the next nodes can map its columns.

Each new row reaches the next node as one item, keyed by the headers: {{ $json.Name }} is the member's name, {{ $json['Home gym'] }} their gym.

3

Fill the form with the Apdf node

  1. In the Apdf dashboard, open API → Tokens and click Create token. In n8n, create Apdf API credentials and paste it as the API Token.
  2. Add an Apdf node after the trigger: Resource PDF, Operation Fill Form, and the form's address as File URL.
  3. Under Fields, click Add field once per column. Enter the field name as Name and drag the matching column into Value, for example member_name → {{ $json.Name }} and home_gym → {{ $json['Home gym'] }}.
  4. Under Options, click Add option and turn on Flatten if the PDF is a final record.

For Maya Thompson's row, this is the request the node sends:

{
    "file": "https://files.fernwoodclimbing.example/forms/membership-agreement.pdf",
    "fields": [
        { "name": "member_name", "value": "Maya Thompson" },
        { "name": "date_of_birth", "value": "1994-06-21" },
        { "name": "email", "value": "maya.thompson@example.com" },
        { "name": "phone", "value": "+1 828 555 0172" },
        { "name": "membership_plan", "value": "Annual" },
        { "name": "home_gym", "value": "Old Town" },
        { "name": "start_date", "value": "2026-11-16" },
        { "name": "belay_certified", "value": "TRUE" },
        { "name": "emergency_contact", "value": "Jordan Thompson (brother)" },
        { "name": "emergency_phone", "value": "+1 828 555 0119" },
        { "name": "newsletter", "value": "FALSE" }
    ],
    "flatten": true
}

And the node's output — a link to the filled PDF:

{
    "file": "https://apdf-files.s3.eu-central-1.amazonaws.com/01d538356ad47a98e6acc.pdf",
    "expiration": "2026-10-18T08:51:53.601747Z",
    "calculations": {
        "updated": [],
        "unsupported": []
    }
}

Fields you leave out keep their value, and calculated fields such as totals are updated after filling (calculations lists them; this form has none). Flatten merges the filled fields into the page: the result looks the same but can't be edited anymore. Leave it off when the recipient still has fields to complete.

Maya Thompson's flattened membership agreement: name, date of birth, email and phone filled, the Annual plan selected, Old Town as home gym, start date 2026-11-16, the belay certification box ticked and her brother as emergency contact
4

Save the PDF to Drive and email it

The Apdf node returns a link, not the file. Download it into the workflow, then hand the file to Drive and Gmail:

  1. Add an HTTP Request node: Method GET, URL {{ $json.file }}. Under Options, add Response and set Response Format to File. The PDF lands in the binary field named under Put Output in Field, data by default.
  2. Connect a Google Drive node with Upload a file: Input Data Field Name data, File Name membership-agreement-{{ $('Google Sheets Trigger').item.json.Name }}.pdf, and the members folder as Parent Folder.
  3. Connect a Gmail node to the HTTP Request node as well, with Send: To {{ $('Google Sheets Trigger').item.json.Email }}, a Subject and Message, and under Options → Attachments the Attachment Field Name data.
  4. Activate the workflow. Every row added from now on gets its own agreement.

Drive and Gmail both hang off the HTTP Request node, so each of them gets the file. Download it in the same run, as this workflow does: the link in file stops working after 60 minutes (see expiration), so don't store it in the sheet.

5

Find out why a row was rejected

When a row carries a value the form doesn't accept, the fill is rejected and nothing is filled. Every call the node makes is listed in the Apdf dashboard under API → Logs — give the n8n workflow its own token and filter the log by it. A rejected call expands to its Response Content, which names the field and the options it takes:

The API Logs page filtered by the n8n token: one Read Form call and four Fill Form calls, one of them a 422 expanded to show that membership_plan expects one of the options Monthly, Annual, Student

Here Hannah Cho's row said “student”. Fix the cell, and the next run fills her agreement. A field name that doesn't exist on the form is reported the same way.

Plan note: Filling forms works on the free plan, which includes 1,000 PDF operations a month — one per filled form. This workflow uses no Apdf automations, so no paid plan is needed.

Where to go from here

Prefilled forms out — and the filled ones coming back can be read just as easily.

After the API call

Your code made the PDF.
Then it went dark.

Opened, read, signed or ignored — you never see any of it. Send the PDFs you generate as personal links, and every open, page read, comment and signature comes back to your software as a webhook. Need it signed? Same link. Same account, same API key, one more call.

Your PDF, after sending Live
document:loaded CFO
page:read p4 · 38s
signature:signed 11:48

API · Webhook · MCP