First, excuse my English for I’m a French Grist enthusiast. I searched for weeks a way to send a PDF file once a form has been submitted in order the user gets a ready-to-print summary of what he’d just filled.
Thanks to a lot of French top-gristers (@aude @Vincent_Viers…), I manage to get a really DIY (and surely really dirty) way using n8n and Gotenberg.
Here is my perfectible but fully functional attempt. Any advice or improvement is sincerely welcome.
1 & 2. To configure the Webhook and the Grist node, I recommend this post (in French, but I’m sure you’ll easily find one in proper English) by @aude : Workflows basiques avec n8n - Ressources et astuces - le forum Grist
3. A first Code node returns the last record from the Grist table.
Two notes :
-
Normally, a good Grist configuration in the previous node would be suffisaient but I didn’t manage to do it in my n8n/Grist instance.
-
This part of the code could be part of the following node but I preferred to separate tome if I one day manage to resolve the issue above.
-
A third one, to be honest : I use DeepSeek to create the javascript parts as I have very, very few notions.
const allRecords = $input.all();
console.log(`📊 Reçu ${allRecords.length} enregistrements`);
if (allRecords.length === 0) return [];
// Affiche les timestamps pour vérifier
allRecords.forEach((record, index) => {
console.log(`Record ${index}: ${record.json.rec_time}`);
});
const lastRecord = allRecords.reduce((newest, current) => {
return new Date(current.json.rec_time) > new Date(newest.json.rec_time)
? current
: newest;
});
console.log(`✅ Dernier enregistrement: ${lastRecord.json.rec_time}`);
return [lastRecord];
4. The second node generates a pdf file :
-
gets the useful values from the Grist node.
-
converts the UNIX/Timestamp dates in dd/mm/yyyyy (


) -
generates an html template and fills it with our values.
-
save the filled template above to an html file
//gets the useful values from the Grist node.
const {
eleve_nom,
eleve_prenom,
eleve_ddn,
resp1_qualite,
resp1_nom,
resp1_prenom,
resp1_adresse,
resp1_cp,
resp1_commune,
} = items[0].json;
// date UNIX/Timestamp => humanly readable
const date = new Date(eleve_ddn * 1000);
const jour = String(date.getDate()).padStart(2, '0');
const mois = String(date.getMonth() + 1).padStart(2, '0');
const annee = String(date.getFullYear());
const dateFormatee = `${jour}/${mois}/${annee}`;
// generates an html template and fills it with our values
const htmlContent = `
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>My HTML document</title>
<style>
</style>
</head>
<body>
${eleve_prenom} ${eleve_nom} est un sacré zozo !
</body>
</html>
`;
//save the filled template above to an html file
return [{
json: {},
binary: {
data: {
data: Buffer.from(htmlContent).toString('base64'),
mimeType: 'text/html',
fileName: 'index.html'
}
}
}];
5. The HTTP Request / Gotenborg node transforms the html file into pdf with the following configuration (sorry for the screenshot, I’ll write it down as soon as I got time) :
6. The Send mail node.
No tricky part, you only have to write the same name for the attachment pdf as in the Gotenberg node (here « example.pdf »).
Interesting to note that you can get values from Grist to compose your email (recipient, sender, subject, body…), with the following syntax :
{{ $('Grist').item.json.recipient_email_adress }}
Make it yours.![]()


