{
  "name": "Generate and email course certificates from Google Sheets with Canva and Gmail",
  "nodes": [
    {
      "id": "0b9c003c-287e-4131-9684-5f1ea6c33dfc",
      "name": "About this template",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        0,
        240
      ],
      "parameters": {
        "width": 460,
        "height": 952,
        "content": "# \ud83c\udf93 Generate and email course certificates from Google Sheets with Canva and Gmail\n\n**Who's it for**\nTraining providers, HR/onboarding teams, course platforms and event organisers on Canva Enterprise who issue personalised certificates at scale.\n\n**How it works**\n1. A coordinator fills in a row (certificate ID, full name, date, course, instructor, email) and sets `ready` to *`YES`*.\n2. Data Autofill fills your certificate brand template with the row's details.\n3. The design is exported as a PDF and downloaded.\n4. The certificate is emailed to the recipient as a **PDF attachment** via Gmail.\n5. The row is updated with the design link and status *`SENT`*.\n\n**Setup**\n- [ ] Create a Google Sheet with columns: `ready`, `certificate_id`, `full_name`, `full_date`, `course_name`, `instructor_name`, `email`, `design_url`, `status`.\n- [ ] In Canva, create a certificate brand template with a dataset: `full_name`, `full_date`, `course_name`, `instructor_name` (all text).\n- [ ] Enter your brand template ID in **Workflow configuration**.\n- [ ] Connect your Google, Canva and Gmail credentials.\n\n**Requirements**\n- Canva Enterprise account (Data Autofill is Enterprise-only)\n- The **verified** community node `@canva/n8n-nodes-canva` (officially provided by Canva)\n- Gmail credential (swap for SMTP or Outlook if preferred)"
      },
      "typeVersion": 1
    },
    {
      "id": "8af70fe2-3a77-4867-8ad5-7af61c03191e",
      "name": "Note \u2014 trigger",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        640,
        240
      ],
      "parameters": {
        "color": 7,
        "width": 472,
        "height": 948,
        "content": "## Google Sheets trigger\n\nThe trigger watches **only the `ready` column** (\u201cColumns to Watch\u201d), and the IF node only lets a row through when `ready` is exactly `YES` \u2014 so half-filled rows never fire.\n\nAfter the certificate is sent, the workflow sets `ready` to `DONE`; a row is never processed twice. To re-send a certificate, set `ready` back to `YES`."
      },
      "typeVersion": 1
    },
    {
      "id": "6b3041e2-6196-4214-9912-58047b783151",
      "name": "Note \u2014 configuration",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1152,
        240
      ],
      "parameters": {
        "color": 7,
        "width": 300,
        "height": 948,
        "content": "## Workflow configuration\n\n`brand_template_id`: the ID of your certificate brand template (Enterprise plan), containing the dataset fields listed on the yellow sticky. Find it in the URL: *`https://www.canva.com/brand/brand-templates/X`*\n\n`spreadsheet_id` / `sheet_id`: derived **automatically** from the trigger's settings \u2014 leave them as they are. Every later node reads the spreadsheet from here, so the sheet is only ever configured once, in the trigger.\n\n`full_date`: normalised here. The Google Sheets trigger can [render dates as numbers](https://docs.n8n.io/integrations/builtin/trigger-nodes/n8n-nodes-base.googlesheetstrigger/common-issues#date-and-time-columns-are-rendering-as-numbers), so this expression converts serials to text like `5 August 2026` - Plain-text dates are unchanged. Adjust the locale (`en-GB`) and format options to your taste."
      },
      "typeVersion": 1
    },
    {
      "id": "2cc0c45e-231c-460f-8f20-6164783c045a",
      "name": "Note \u2014 Canva",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1488,
        240
      ],
      "parameters": {
        "color": 7,
        "width": 492,
        "height": 944,
        "content": "## Create & export the certificate\n\nData Autofill fills the brand template with the row's details \u2014 field names in the Autofill node must match your template's dataset **exactly**. Autofill replaces each tagged element's *entire* text, so keep static labels (e.g. \u201cDate:\u201d) out of tagged elements.\n\nThe design is then exported as a PDF and downloaded, because export links **expire after 24 hours** \u2014 attaching the file means the recipient's copy never dies.\n\n[Guide on Autofill and Brand Templates](https://www.canva.dev/docs/connect/autofill-guide/)"
      },
      "typeVersion": 1
    },
    {
      "id": "2f06fa97-c872-4784-86b7-223caa0a2a9d",
      "name": "Note \u2014 email",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        2016,
        240
      ],
      "parameters": {
        "color": 7,
        "width": 448,
        "height": 944,
        "content": "## Email the recipient\n\nGmail sends the certificate to the `email` address from the row, with the PDF attached.\n\nPersonalise the subject and body in this node \u2014 or swap it for **SMTP (Send Email)** or **Microsoft Outlook** without touching the rest of the workflow."
      },
      "typeVersion": 1
    },
    {
      "id": "b95f3f38-d4ee-43e8-99b9-487b5a2cd90e",
      "name": "Note \u2014 write back",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        2496,
        240
      ],
      "parameters": {
        "color": 7,
        "width": 396,
        "height": 944,
        "content": "## Write results back\n\nOn success the row is updated:\n\n`ready`: *`DONE`*\n`design_url`: *`{{URL_FROM_PREVIOUS_STEP}}`*\n`status`: *`SENT`*\n\nIf any Canva or email step fails, the row is marked as:\n\n`ready`: *`ERROR`*\n`status`: *`{{THE_ERROR_MESSAGE}}`*\n\nTo retry: fix the cause, then set `ready` back to *`YES`*."
      },
      "typeVersion": 1
    },
    {
      "id": "07a7fa31-82d7-45c4-9155-77ee12fed904",
      "name": "On Sheet row update",
      "type": "n8n-nodes-base.googleSheetsTrigger",
      "position": [
        720,
        976
      ],
      "parameters": {
        "event": "anyUpdate",
        "options": {
          "columnsToWatch": [
            "ready"
          ]
        },
        "pollTimes": {
          "item": [
            {
              "mode": "everyMinute"
            }
          ]
        },
        "sheetName": {
          "__rl": true,
          "mode": "list",
          "value": ""
        },
        "documentId": {
          "__rl": true,
          "mode": "url",
          "value": "https://docs.google.com/spreadsheets/d/YOUR_DOCUMENT_ID/edit"
        }
      },
      "typeVersion": 1
    },
    {
      "id": "55de4e45-4fcb-45a6-a5c3-266635ea0c94",
      "name": "Row marked ready?",
      "type": "n8n-nodes-base.if",
      "position": [
        912,
        976
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 1,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "cond-ready-01",
              "operator": {
                "type": "string",
                "operation": "equals"
              },
              "leftValue": "={{ ($json.ready ?? '').toString().trim().toUpperCase() }}",
              "rightValue": "YES"
            }
          ]
        }
      },
      "typeVersion": 2
    },
    {
      "id": "8a7b2275-a78d-4f92-8902-b8b6cebb2ba3",
      "name": "Create certificate from brand template",
      "type": "@canva/n8n-nodes-canva.canva",
      "onError": "continueErrorOutput",
      "position": [
        1568,
        960
      ],
      "parameters": {
        "data": "={{ JSON.stringify({\n  full_name: { type: 'text', text: ($('Workflow configuration').item.json.full_name ?? '').toString() },\n  full_date: { type: 'text', text: ($('Workflow configuration').item.json.full_date ?? '').toString() },\n  course_name: { type: 'text', text: ($('Workflow configuration').item.json.course_name ?? '').toString() },\n  instructor_name: { type: 'text', text: ($('Workflow configuration').item.json.instructor_name ?? '').toString() }\n}) }}",
        "title": "={{ 'Certificate \u2014 ' + $('Workflow configuration').item.json.full_name + ' \u2014 ' + $('Workflow configuration').item.json.course_name }}",
        "resource": "autofill",
        "brandTemplateId": "={{ $('Workflow configuration').item.json.brand_template_id }}"
      },
      "typeVersion": 1
    },
    {
      "id": "7f0caac3-d6c5-4cad-8d7d-a9af3754f866",
      "name": "Export certificate as PDF",
      "type": "@canva/n8n-nodes-canva.canva",
      "onError": "continueErrorOutput",
      "position": [
        1792,
        960
      ],
      "parameters": {
        "maxWait": 120,
        "designId": "={{ $json.job.result.design.id }}",
        "resource": "export"
      },
      "typeVersion": 1
    },
    {
      "id": "8abb64f2-4cc9-48bc-9cda-802fae660d39",
      "name": "Download the PDF",
      "type": "n8n-nodes-base.httpRequest",
      "onError": "continueErrorOutput",
      "position": [
        2096,
        960
      ],
      "parameters": {
        "url": "={{ $json.job.urls[0] }}",
        "options": {
          "response": {
            "response": {
              "responseFormat": "file"
            }
          }
        }
      },
      "typeVersion": 4.2
    },
    {
      "id": "8423e930-af86-44aa-ae52-9251ace80139",
      "name": "Email certificate to recipient",
      "type": "n8n-nodes-base.gmail",
      "onError": "continueErrorOutput",
      "position": [
        2320,
        960
      ],
      "parameters": {
        "sendTo": "={{ $('Workflow configuration').item.json.email }}",
        "message": "=<p>Hi {{ $('Workflow configuration').item.json.full_name }},</p><p>Congratulations on completing <strong>{{ $('Workflow configuration').item.json.course_name }}</strong>!</p><p>Your certificate is attached as a PDF. Keep it safe \u2014 and feel free to share it.</p><p>Best,<br>{{ $('Workflow configuration').item.json.instructor_name }}</p>",
        "options": {
          "attachmentsUi": {
            "attachmentsBinary": [
              {
                "property": "data"
              }
            ]
          },
          "appendAttribution": false
        },
        "subject": "=Your certificate \u2014 {{ $('Workflow configuration').item.json.course_name }}"
      },
      "typeVersion": 2.1
    },
    {
      "id": "925aa173-8c27-40bf-9477-8e7389ec6f39",
      "name": "Mark row as errored",
      "type": "n8n-nodes-base.googleSheets",
      "position": [
        2640,
        976
      ],
      "parameters": {
        "columns": {
          "value": {
            "ready": "ERROR",
            "status": "={{ 'Step failed: ' + ($json.error?.message ?? 'check the execution log in n8n') }}",
            "certificate_id": "={{ $('Workflow configuration').item.json.certificate_id }}"
          },
          "schema": [
            {
              "id": "ready",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "ready",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "certificate_id",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "certificate_id",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "full_name",
              "type": "string",
              "display": true,
              "removed": true,
              "required": false,
              "displayName": "full_name",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "full_date",
              "type": "string",
              "display": true,
              "removed": true,
              "required": false,
              "displayName": "full_date",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "course_name",
              "type": "string",
              "display": true,
              "removed": true,
              "required": false,
              "displayName": "course_name",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "instructor_name",
              "type": "string",
              "display": true,
              "removed": true,
              "required": false,
              "displayName": "instructor_name",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "email",
              "type": "string",
              "display": true,
              "removed": true,
              "required": false,
              "displayName": "email",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "design_url",
              "type": "string",
              "display": true,
              "removed": true,
              "required": false,
              "displayName": "design_url",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "status",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "status",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "row_number",
              "type": "number",
              "display": true,
              "removed": true,
              "readOnly": true,
              "required": false,
              "displayName": "row_number",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            }
          ],
          "mappingMode": "defineBelow",
          "matchingColumns": [
            "certificate_id"
          ],
          "attemptToConvertTypes": false,
          "convertFieldsToString": false
        },
        "options": {},
        "operation": "update",
        "sheetName": {
          "__rl": true,
          "mode": "id",
          "value": "={{ $('Workflow configuration').item.json.sheet_id }}"
        },
        "documentId": {
          "__rl": true,
          "mode": "id",
          "value": "={{ $('Workflow configuration').item.json.spreadsheet_id }}"
        }
      },
      "typeVersion": 4.5
    },
    {
      "id": "323c165c-c5de-4fa9-90dd-f0fa15fa1c84",
      "name": "Workflow configuration",
      "type": "n8n-nodes-base.set",
      "position": [
        1248,
        960
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "cfg-0001",
              "name": "brand_template_id",
              "type": "string",
              "value": "YOUR_BRAND_TEMPLATE_ID"
            },
            {
              "id": "cfg-0002",
              "name": "spreadsheet_id",
              "type": "string",
              "value": "={{ ($('On Sheet row update').params.documentId.value || '').toString().match(/[-\\w]{25,}/)?.[0] }}"
            },
            {
              "id": "cfg-0003",
              "name": "sheet_id",
              "type": "string",
              "value": "={{ $('On Sheet row update').params.sheetName.value }}"
            },
            {
              "id": "cfg-0004",
              "name": "full_date",
              "type": "string",
              "value": "={{ ($json.full_date ?? '').toString().trim() === '' ? '' : (isNaN(Number($json.full_date)) ? $json.full_date.toString() : new Date(Date.UTC(1899, 11, 30) + Math.round(Number($json.full_date) * 86400000)).toLocaleDateString('en-GB', { day: 'numeric', month: 'long', year: 'numeric', timeZone: 'UTC' })) }}"
            }
          ]
        },
        "includeOtherFields": true
      },
      "typeVersion": 3.4
    },
    {
      "id": "2b363797-7d20-4f05-abc3-85f3eea5455a",
      "name": "Mark row as completed",
      "type": "n8n-nodes-base.googleSheets",
      "position": [
        2640,
        720
      ],
      "parameters": {
        "columns": {
          "value": {
            "ready": "DONE",
            "status": "SENT",
            "design_url": "={{ $('Create certificate from brand template').item.json.job.result.design.url }}",
            "certificate_id": "={{ $('Workflow configuration').item.json.certificate_id }}"
          },
          "schema": [
            {
              "id": "ready",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "ready",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "certificate_id",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "certificate_id",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "full_name",
              "type": "string",
              "display": true,
              "removed": true,
              "required": false,
              "displayName": "full_name",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "full_date",
              "type": "string",
              "display": true,
              "removed": true,
              "required": false,
              "displayName": "full_date",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "course_name",
              "type": "string",
              "display": true,
              "removed": true,
              "required": false,
              "displayName": "course_name",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "instructor_name",
              "type": "string",
              "display": true,
              "removed": true,
              "required": false,
              "displayName": "instructor_name",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "email",
              "type": "string",
              "display": true,
              "removed": true,
              "required": false,
              "displayName": "email",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "design_url",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "design_url",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "status",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "status",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "row_number",
              "type": "number",
              "display": true,
              "removed": true,
              "readOnly": true,
              "required": false,
              "displayName": "row_number",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            }
          ],
          "mappingMode": "defineBelow",
          "matchingColumns": [
            "certificate_id"
          ],
          "attemptToConvertTypes": false,
          "convertFieldsToString": false
        },
        "options": {},
        "operation": "update",
        "sheetName": {
          "__rl": true,
          "mode": "id",
          "value": "={{ $('Workflow configuration').item.json.sheet_id }}"
        },
        "documentId": {
          "__rl": true,
          "mode": "id",
          "value": "={{ $('Workflow configuration').item.json.spreadsheet_id }}"
        }
      },
      "typeVersion": 4.5
    }
  ],
  "active": false,
  "settings": {
    "executionOrder": "v1"
  },
  "connections": {
    "Download the PDF": {
      "main": [
        [
          {
            "node": "Email certificate to recipient",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Mark row as errored",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Row marked ready?": {
      "main": [
        [
          {
            "node": "Workflow configuration",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "On Sheet row update": {
      "main": [
        [
          {
            "node": "Row marked ready?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Workflow configuration": {
      "main": [
        [
          {
            "node": "Create certificate from brand template",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Export certificate as PDF": {
      "main": [
        [
          {
            "node": "Download the PDF",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Mark row as errored",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Email certificate to recipient": {
      "main": [
        [
          {
            "node": "Mark row as completed",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Mark row as errored",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Create certificate from brand template": {
      "main": [
        [
          {
            "node": "Export certificate as PDF",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Mark row as errored",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}