Auto-Fill & Flatten PDF Forms from Google Form → Google Sheets → PDF.co (n8n)

Nov 14, 2025·6 Minutes Read

What you’ll have when done

A no-touch pipeline that:

  1. Listens for new Google Form submissions (arriving as new rows in Google Sheets).
  2. Maps fields from Sheets into a PDF form using PDF.co.
  3. Flattens the filled PDF automatically via the PDF.co Profiles feature: { 'FlattenDocument()': [] } (This converts form fields to regular content so they cannot be edited.)
  4. Downloads the filled+flattened PDF as binary.
  5. Optionally emails the PDF and/or uploads it to Google Drive with a clean filename.

Prerequisites

  • PDF.co API Key (add in n8n credentials or use HTTP headers).Get yours here : https://app.pdf.co/
  • n8n (cloud or self-hosted).
  • Google Sheets prepared as a target for Google Form responses.
  • A fillable PDF form. Here is a sample Form you can test with.
  • Field names & page indexes for the PDF form (get them via PDF Inspector).

Get your PDF field names (with PDF Inspector)

  1. Open PDF Inspector: https://app.pdf.co/pdf-inspector
  2. Upload your fillable PDF.
  3. Click each field on the page. On the left pane, you’ll see details such as:
    • fieldName (exact name, case sensitive),
    • pageIndex (starts from 0 on PDF.co).
  4. Copy these names—you’ll use them in the Fill PDF Form call.

Tip: Save the field list somewhere (Notion/Google Doc). You’ll reference them when building the “fields” array in the API call.

Endpoints we’ll use (PDF.co)

We’ll use the Fill PDF Form endpoint (and optionally Email Send):

  1. Fill PDF Form + Flatten

PDF.co docs label this under “PDF Add”. Your custom node title “Fill a PDF Form” maps to this endpoint.

  1. Send Email with Attachment (optional)

Quick Start Options

Option A: I Want It Working Now

  1. Import this workflow template → copy the JSON (Link Here) into a file and import it in n8n.
  2. Connect Google Sheets (for the trigger) and Google Drive (for upload).
  3. Add your PDF.co API key in the “PDFco – Fill & Flatten (POST)” HTTP node headers.
  4. Update the Google Sheets Trigger with your Sheet (the one receiving Google Form responses).
  5. Update the Google Drive Upload node with your destination folder ID.
  6. Put your PDF template URL in the HTTP body (templatePdfUrl).
  7. Map field names from PDF Inspector (fieldName / page).
  8. (Optional) Enable Email node if you want the filled PDF emailed automatically.
  9. Test by submitting the Google Form, then Activate the workflow.

Option B : Step-by-Step Build Guide

Step 1: Trigger on new Google Form submissions (arriving as new rows)

Node: Google Sheets Trigger

What it does: Fires when a new row is added to Sheet1.

Settings

  • Document: Select your Google Sheet (the one receiving Google Form responses).
  • Sheet: Sheet1 (or your actual tab).
  • Event: Row Added

Success looks like: When someone submits the Google Form, a new row appears in Sheets and this node runs, passing the row’s columns as JSON.

Step 2: (Optional) Normalize dates coming from the sheet

If your form captures dates as MM/DD/YYYY, convert to YYYY-MM-DD for consistency (good for filenames and records).

Node: Code

What it does: Scans incoming fields and adds an (ISO) version where it finds US-style dates.

Paste this JS in the Code node:

/**
 * n8n Code Node
 * Converts MM/DD/YYYY → ISO (YYYY-MM-DD)
 * - Valid dates: keep original and add "<Field> (ISO)"
 * - Invalid dates: remove original, add "<Field> (error)" = "nonexistent-calendar-date"
 */

function parseUsDateStrict(str) {
  if (typeof str !== 'string') return { ok: false };

  // Match pattern loosely; we'll validate logically afterward
  const re = /^(?<m>0?[1-9]|1[0-2])\/(?<d>\d{1,2})\/(?<y>\d{4})$/;
  const match = str.match(re);
  if (!match) return { ok: false };

  const month = parseInt(match.groups.m, 10);
  const day = parseInt(match.groups.d, 10);
  const year = parseInt(match.groups.y, 10);

  // Quick sanity range
  if (day < 1 || month < 1 || month > 12 || year < 1000) return { ok: false };

  // Check if date actually exists
  const dt = new Date(year, month - 1, day);
  const isReal =
    dt.getFullYear() === year &&
    dt.getMonth() === month - 1 &&
    dt.getDate() === day;

  if (!isReal) return { ok: false };

  // Return ISO format
  return {
    ok: true,
    iso: `${year}-${String(month).padStart(2, '0')}-${String(day).padStart(2, '0')}`
  };
}

return items.map(item => {
  const input = { ...item.json };
  const output = {};

  for (const key of Object.keys(input)) {
    const val = input[key];

    const looksLikeDate = typeof val === 'string' && /\d{1,2}\/\d{1,2}\/\d{2,4}/.test(val);

    if (looksLikeDate) {
      const res = parseUsDateStrict(val);

      if (res.ok) {
        // Valid date: keep original and ISO
        output[key] = val;
        output[`${key} (ISO)`] = res.iso;
      } else {
        // Invalid: remove original, add uniform error
        output[`${key} (error)`] = 'nonexistent-calendar-date';
      }
    } else {
      // Pass through non-date fields untouched
      output[key] = val;
    }
  }

  return { json: output };
});

Success looks like: Your item now includes fields like Created At (ISO) from Created At.

Step 3: Fill the PDF form and flatten it with PDF.co

You can use your custom PDF.co node or a generic HTTP Request (POST). I’ll show both.

Option A: Your custom PDFco node (“Fill a PDF Form”)

Node: PDFco Api (Fill a PDF Form)

What it does: Fills fields and applies Profiles to flatten.

Key parameters (match to your sample):

  • Operation: Fill a PDF Form
  • Source (url/filetoken): The PDF to fill (filetoken or URL).
  • Fields → metadataValues: list of fields with fieldName matching the PDF.
  • Advanced Options → name: output PDF name.
  • Advanced Options → profiles: { 'FlattenDocument()': [] }

Example (from your sample, adapted):

// Fields: use exact fieldName from PDF Inspector, and values from the Sheet row

Asignee_Name ← $json["Asignee's Name"]

Created_at ← $json["Created At (ISO)"] or $json["Created At"]

Due_Date ← $json["Due on (ISO)"] or $json["Due on"]

Task ← $json["Task"]

Advanced Options:

name = Assigned Task to {{ $json["Asignee's Name"] }}

profiles = { 'FlattenDocument()': [] }

What flatten does: The Profile FlattenDocument() renders filled form fields into the page content so they cannot be edited later in a PDF viewer.

Success looks like: The node returns JSON with url pointing to the filled + flattened PDF.

Option B: Use HTTP Request (POST) directly

Node: HTTP Request (POST to PDF.co) Headers:

  • x-api-key: <YOUR_PDFCO_API_KEY>
  • Content-Type: application/json

URL: https://api.pdf.co/v1/pdf/edit/add

Body (JSON): (edit field names and URL/file per your file source)

{
  "url": "{{ $json.pdfUrl || 'https://YOUR_STORAGE/form-template.pdf' }}",
  "name": "Assigned Task to {{ $json['Asignee\\'s Name'] }}",
  "fields": [
    { "fieldName": "Asignee_Name", "pages": "0-", "text": "{{ $json['Asignee\\'s Name'] }}", "fontName": "Arial", "size": 9 },
    { "fieldName": "Created_at",   "pages": "0-", "text": "{{ $json['Created At (ISO)'] || $json['Created At'] }}", "fontName": "Arial", "size": 9 },
    { "fieldName": "Due_Date",     "pages": "0-", "text": "{{ $json['Due on (ISO)'] || $json['Due on'] }}", "fontName": "Arial", "size": 9 },
    { "fieldName": "Task",         "pages": "0-", "text": "{{ $json['Task'] }}", "fontName": "Arial", "size": 9 }
  ],
  "profiles": "{ 'FlattenDocument()': [] }"
}

Notes:

  • pages: "0-" means “all pages from page index 0”. If your field is on a specific page, you can set "pages": "0" etc.
  • If your template is stored in n8n binary, first upload it to a temp storage (PDF.co file/upload or your own) and pass the URL here.

Success looks like: JSON with a url to the filled+flattened PDF.

Step 4: Download the result as binary (so Drive can upload it)

Node: HTTP Request (GET)

What it does: Takes the url returned by the fill step and downloads the finished PDF as binary.

Settings

  • URL: ={{ $json.url }}
  • Method: GET
  • Options → Add Option → Response Format: File
  • Options → Add Option → Binary Property: data

Success looks like: The execution shows a Binary tab with data.

Step 5: Upload to Google Drive

Node: Google Drive → Upload File

What it does: Saves the finished PDF to your OUTPUT folder.

Settings

  • Resource: File
  • Operation: Upload
  • Input Data Field Name (Binary Property): data
  • Parent Drive: My Drive
  • Parent Folder by ID: (pick your output folder)

If you want a standardized filename with the date, either set name in the PDF.co call (as we did) or add a Set node before upload to specify name like:

{{ 'Assigned Task to ' + $json["Asignee's Name"] + ' - ' + ($json["Created At (ISO)"] || $json["Created At"]) + '.pdf' }}


Step 6: (Optional) Email the PDF via PDF.co

Node: HTTP Request (POST)

URL: https://api.pdf.co/v1/email/send

Headers: x-api-key, Content-Type: application/json

Body (JSON)Option B (valid JSON, single line, escaped):

{
  "url": "{{ $json.url }}",
  "from": "your_email@gmail.com",
  "to": "recipient@example.com",
  "subject": "New Task Assigned: {{ $('Code').item.json.Task }}",
  "bodyHtml": "<p>Hi,</p><p>Please find the attached, filled and flattened PDF form.</p><ul><li><strong>Task:</strong> {{ $('Code').item.json.Task }}</li><li><strong>Assignee:</strong> {{ $('Code').item.json['Asignee\\'s Name'] }}</li><li><strong>Created On:</strong> {{ $('Code').item.json['Created At (ISO)'] || $('Code').item.json['Created At'] }}</li><li><strong>Due On:</strong> {{ $('Code').item.json['Due on (ISO)'] || $('Code').item.json['Due on'] }}</li></ul><p>Best regards,<br>Project Automation Team</p>",
  "smtpserver": "smtp.gmail.com",
  "smtpport": "587",
  "smtpusername": "your_email@gmail.com",
  "smtppassword": "your_app_password",
  "async": false
}


Congratulations! You’ve successfully automated PDF form filling and flattening. Your workflow now:

  • Captures submissions from Google Forms (via Google Sheets new rows).
  • Normalizes dates to YYYY-MM-DD for clean, auditable filenames.
  • Fills your PDF template fields with Sheet data using PDF.co (Endpoint: POST /v1/pdf/edit/add).
  • Flattens the document automatically with Profiles: { 'FlattenDocument()': [] } so fields can’t be edited.
  • Downloads the finished PDF as binary and uploads it to your chosen Google Drive folder.
  • (Optional) Emails the filled & flattened PDF using PDF.co Email (Endpoint: POST /v1/email/send).

This setup is robust and scalable—from small teams to enterprise workflows. Built something cool with this flow? Share it with us @pdfdotco

Related Tutorials

See Related Tutorials