This workflow follows the Airtable → HTTP Request recipe pattern — see all workflows that pair these two integrations.
The workflow JSON
Copy or download the full n8n JSON below. Paste it into a new n8n workflow, add your credentials, activate. Full import guide →
{
"active": false,
"connections": {
"R\u00e9ception de Document": {
"main": [
[
{
"node": "Classification OCR",
"type": "main",
"index": 0
}
]
]
},
"Classification OCR": {
"main": [
[
{
"node": "Enregistrement Airtable",
"type": "main",
"index": 0
}
]
]
},
"Enregistrement Airtable": {
"main": [
[
{
"node": "V\u00e9rif Document Expir\u00e9",
"type": "main",
"index": 0
}
]
]
},
"V\u00e9rif Document Expir\u00e9": {
"main": [
[
{
"node": "Alerte Document Expir\u00e9",
"type": "main",
"index": 0
}
]
]
},
"Scanner Quotidien": {
"main": [
[
{
"node": "Liste Documents",
"type": "main",
"index": 0
}
]
]
},
"Liste Documents": {
"main": [
[
{
"node": "V\u00e9rifier Expiration Imminente",
"type": "main",
"index": 0
}
]
]
},
"V\u00e9rifier Expiration Imminente": {
"main": [
[
{
"node": "Traiter Individuellement",
"type": "main",
"index": 0
}
]
]
},
"Traiter Individuellement": {
"main": [
[
{
"node": "Envoyer Alerte Expiration",
"type": "main",
"index": 0
}
]
]
},
"D\u00e9clencheur Hebdomadaire": {
"main": [
[
{
"node": "Scraper Veille R\u00e9glementaire",
"type": "main",
"index": 0
},
{
"node": "Liste Formateurs",
"type": "main",
"index": 0
},
{
"node": "Liste Indicateurs",
"type": "main",
"index": 0
}
]
]
},
"Liste Indicateurs": {
"main": [
[
{
"node": "Liste Documents pour Audit",
"type": "main",
"index": 0
}
]
]
},
"Liste Documents pour Audit": {
"main": [
[
{
"node": "Analyse de Conformit\u00e9",
"type": "main",
"index": 0
}
]
]
},
"Analyse de Conformit\u00e9": {
"main": [
[
{
"node": "Enregistrer Rapport",
"type": "main",
"index": 0
}
]
]
},
"Enregistrer Rapport": {
"main": [
[
{
"node": "Diffuser Rapport",
"type": "main",
"index": 0
}
]
]
},
"V\u00e9rifier Seuil d'Alerte": {
"main": [
[
{
"node": "Envoi Alerte Conformit\u00e9",
"type": "main",
"index": 0
}
]
]
},
"Liste Formateurs": {
"main": [
[
{
"node": "V\u00e9rifier Certifications",
"type": "main",
"index": 0
}
]
]
},
"V\u00e9rifier Certifications": {
"main": [
[
{
"node": "Traiter Par Formateur",
"type": "main",
"index": 0
}
]
]
},
"Traiter Par Formateur": {
"main": [
[
{
"node": "Alerte Expiration Certification",
"type": "main",
"index": 0
}
]
]
},
"Scraper Veille R\u00e9glementaire": {
"main": [
[
{
"node": "Analyser Changements",
"type": "main",
"index": 0
}
]
]
},
"Analyser Changements": {
"main": [
[
{
"node": "Si Changements D\u00e9tect\u00e9s",
"type": "main",
"index": 0
}
]
]
},
"Si Changements D\u00e9tect\u00e9s": {
"main": [
[
{
"node": "Enregistrer Veille",
"type": "main",
"index": 0
},
{
"node": "Notification Veille",
"type": "main",
"index": 0
}
]
]
},
"D\u00e9clencheur Nouvelle Formation": {
"main": [
[
{
"node": "Pr\u00e9parer Donn\u00e9es Formation",
"type": "main",
"index": 0
}
]
]
},
"Regrouper Documents": {
"main": [
[
{
"node": "Enregistrer Documents",
"type": "main",
"index": 0
}
]
]
},
"D\u00e9clencheur Formation Termin\u00e9e": {
"main": [
[]
]
},
"Pr\u00e9parer \u00c9valuations": {
"main": [
[
{
"node": "Par Participant",
"type": "main",
"index": 0
}
]
]
},
"Par Participant": {
"main": [
[
{
"node": "Envoyer Questionnaire \u00c9valuation",
"type": "main",
"index": 0
}
]
]
},
"Envoyer Questionnaire \u00c9valuation": {
"main": [
[
{
"node": "Enregistrer Envoi \u00c9valuation",
"type": "main",
"index": 0
}
]
]
},
"R\u00e9ception \u00c9valuation": {
"main": [
[
{
"node": "Analyser \u00c9valuation",
"type": "main",
"index": 0
}
]
]
},
"Analyser \u00c9valuation": {
"main": [
[
{
"node": "Enregistrer R\u00e9sultats",
"type": "main",
"index": 0
}
]
]
},
"Enregistrer R\u00e9sultats": {
"main": [
[
{
"node": "V\u00e9rifier \u00c9valuation Insatisfaisante",
"type": "main",
"index": 0
}
]
]
},
"V\u00e9rifier \u00c9valuation Insatisfaisante": {
"main": [
[
{
"node": "Cr\u00e9er Action Corrective",
"type": "main",
"index": 0
}
]
]
},
"Cr\u00e9er Action Corrective": {
"main": [
[
{
"node": "Notifier Responsable Qualit\u00e9",
"type": "main",
"index": 0
}
]
]
},
"G\u00e9n\u00e9ration Rapports Mensuels": {
"main": [
[
{
"node": "R\u00e9cup\u00e9rer \u00c9valuations R\u00e9centes",
"type": "main",
"index": 0
}
]
]
},
"R\u00e9cup\u00e9rer \u00c9valuations R\u00e9centes": {
"main": [
[
{
"node": "G\u00e9n\u00e9rer Rapport Mensuel",
"type": "main",
"index": 0
}
]
]
},
"When clicking \u2018Test workflow\u2019": {
"main": [
[
{
"node": "Pr\u00e9parer \u00c9valuations",
"type": "main",
"index": 0
}
]
]
}
},
"createdAt": "2025-05-22T12:39:17.913Z",
"id": "p4n8rgT4RWrRAj6a",
"isArchived": false,
"meta": null,
"name": "qualiopi",
"nodes": [
{
"parameters": {
"path": "document-reception",
"options": {}
},
"name": "R\u00e9ception de Document",
"type": "n8n-nodes-base.webhook",
"typeVersion": 1,
"position": [
-1300,
-360
],
"id": "11dd27f0-a9e7-417f-9016-23e403bb134e"
},
{
"parameters": {
"functionCode": "// Analyse et classement par OCR\nconst document = items[0].json;\nconst documentContent = document.content || \"\";\n\n// Simulation d'une analyse OCR & IA\nlet category = \"Divers\";\nlet indicators = [];\n\nif(documentContent.includes(\"convention\") || document.type === \"convention\") {\n category = \"Conventions\";\n indicators = [\"1.1\", \"1.2\"];\n} else if(documentContent.includes(\"attestation\") || document.type === \"attestation\") {\n category = \"Attestations\";\n indicators = [\"3.1\", \"3.2\"];\n} else if(documentContent.includes(\"programme\") || document.type === \"programme\") {\n category = \"Programmes\";\n indicators = [\"2.1\"];\n} else if(documentContent.includes(\"\u00e9valuation\") || document.type === \"evaluation\") {\n category = \"\u00c9valuations\";\n indicators = [\"2.3\", \"7.2\"];\n} else if(documentContent.includes(\"formateur\") || document.type === \"cv\") {\n category = \"Formateurs\";\n indicators = [\"5.1\", \"5.2\"];\n}\n\nreturn [\n {\n json: {\n ...document,\n category,\n relatedIndicators: indicators,\n classificationDate: new Date().toISOString(),\n isClassified: true\n }\n }\n];"
},
"name": "Classification OCR",
"type": "n8n-nodes-base.function",
"typeVersion": 1,
"position": [
-1100,
-360
],
"id": "4cf1a4d7-2c9d-4795-a708-690c215ce430"
},
{
"parameters": {
"application": {
"__rl": true,
"mode": "url",
"value": ""
},
"table": "Documents Qualiopi"
},
"name": "Enregistrement Airtable",
"type": "n8n-nodes-base.airtable",
"typeVersion": 1,
"position": [
-900,
-360
],
"id": "ba0ceb55-b412-4202-8913-cf809dda34e1"
},
{
"parameters": {
"conditions": {
"string": [
{
"value1": "={{$json.status}}",
"operation": "equals",
"value2": "expired"
}
]
}
},
"name": "V\u00e9rif Document Expir\u00e9",
"type": "n8n-nodes-base.if",
"typeVersion": 1,
"position": [
-700,
-360
],
"id": "2be724cb-ed99-4f87-b99f-98e2d5f2aebc"
},
{
"parameters": {
"chatId": "={{$env.TELEGRAM_CHAT_ID}}",
"text": "\u26a0\ufe0f DOCUMENT EXPIR\u00c9 \u26a0\ufe0f\n\nLe document \"{{$json.reference}}\" de type \"{{$json.type}}\" est expir\u00e9 depuis le {{$json.expirationDate}}.\n\nVeuillez le renouveler pour maintenir la conformit\u00e9 Qualiopi.",
"additionalFields": {}
},
"name": "Alerte Document Expir\u00e9",
"type": "n8n-nodes-base.telegram",
"typeVersion": 1,
"position": [
-500,
-460
],
"id": "9715e9c3-94e0-483f-8368-af22bd7ee722"
},
{
"parameters": {
"rule": {
"interval": [
{}
]
}
},
"name": "Scanner Quotidien",
"type": "n8n-nodes-base.scheduleTrigger",
"typeVersion": 1,
"position": [
-1300,
-60
],
"id": "8ab21f7e-2dcf-41bc-80e7-631a74b696d7"
},
{
"parameters": {
"application": {
"__rl": true,
"mode": "url",
"value": ""
},
"table": "Documents Qualiopi"
},
"name": "Liste Documents",
"type": "n8n-nodes-base.airtable",
"typeVersion": 1,
"position": [
-1100,
-60
],
"id": "8de4e7d4-ce88-4da7-bca2-dcee53896908"
},
{
"parameters": {
"functionCode": "// V\u00e9rifier les docs qui vont bient\u00f4t expirer (dans les 30 jours)\nconst documents = items;\nconst today = new Date();\nconst warningDocs = [];\n\nfor (const doc of documents) {\n if (doc.json.expirationDate) {\n const expDate = new Date(doc.json.expirationDate);\n const diffTime = expDate - today;\n const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24));\n \n if (diffDays <= 30 && diffDays > 0) {\n warningDocs.push({\n json: {\n ...doc.json,\n daysUntilExpiration: diffDays\n }\n });\n }\n }\n}\n\nreturn warningDocs;"
},
"name": "V\u00e9rifier Expiration Imminente",
"type": "n8n-nodes-base.function",
"typeVersion": 1,
"position": [
-900,
-60
],
"id": "328cf05b-785b-4186-b7dd-78a7c37b0791"
},
{
"parameters": {
"batchSize": 1,
"options": {}
},
"name": "Traiter Individuellement",
"type": "n8n-nodes-base.splitInBatches",
"typeVersion": 1,
"position": [
-700,
-60
],
"id": "4647a618-851e-42f4-bca4-4588e1847a52"
},
{
"parameters": {
"subject": "Alerte Qualiopi - Document expirant dans {{$json.daysUntilExpiration}} jours",
"text": "Bonjour,\n\nLe document \"{{$json.reference}}\" de type \"{{$json.type}}\" va expirer dans {{$json.daysUntilExpiration}} jours.\n\nCe document est li\u00e9 aux indicateurs Qualiopi suivants : {{$json.relatedIndicators.join(\", \")}}.\n\nMerci de pr\u00e9voir son renouvellement pour maintenir la conformit\u00e9.\n\nCordialement,\nSyst\u00e8me d'automatisation Qualiopi",
"options": {}
},
"name": "Envoyer Alerte Expiration",
"type": "n8n-nodes-base.emailSend",
"typeVersion": 1,
"position": [
-500,
-60
],
"id": "b7426775-37e9-43ca-a21d-770ef9def40a"
},
{
"parameters": {
"application": {
"__rl": true,
"mode": "url",
"value": ""
},
"table": "Indicateurs Qualiopi"
},
"name": "Liste Indicateurs",
"type": "n8n-nodes-base.airtable",
"typeVersion": 1,
"position": [
-1100,
140
],
"id": "428511c1-92f9-4002-b002-8c4bc75b5b42"
},
{
"parameters": {
"application": {
"__rl": true,
"mode": "url",
"value": ""
},
"table": "Documents Qualiopi"
},
"name": "Liste Documents pour Audit",
"type": "n8n-nodes-base.airtable",
"typeVersion": 1,
"position": [
-900,
140
],
"id": "1d9f80ba-f317-4851-bab8-29fde22be001"
},
{
"parameters": {
"functionCode": "// Analyse de conformit\u00e9\nconst indicators = items[0].json.data; // tous les indicateurs\nconst documents = items[1].json.data; // tous les documents\n\nconst conformityReport = [];\nlet globalConformity = 0;\n\n// Pour chaque indicateur, v\u00e9rifier les documents associ\u00e9s\nfor (const indicator of indicators) {\n const indicatorCode = indicator.fields.Code;\n const requiredDocs = indicator.fields.DocumentsRequis?.split(',').map(d => d.trim()) || [];\n \n // Compter combien de types requis sont couverts\n let coveredDocsCount = 0;\n let associatedDocs = [];\n \n for (const reqDoc of requiredDocs) {\n const matchingDocs = documents.filter(d => {\n return d.fields.type === reqDoc || \n (d.fields.relatedIndicators && d.fields.relatedIndicators.includes(indicatorCode));\n });\n \n if (matchingDocs.length > 0) {\n coveredDocsCount++;\n associatedDocs = [...associatedDocs, ...matchingDocs.map(d => d.fields.reference)];\n }\n }\n \n // Calculer le taux de conformit\u00e9 pour cet indicateur\n const conformityRate = requiredDocs.length > 0 \n ? Math.round((coveredDocsCount / requiredDocs.length) * 100) \n : 100;\n \n conformityReport.push({\n indicatorCode,\n indicatorName: indicator.fields.Libelle,\n conformityRate,\n requiredDocs,\n associatedDocs,\n status: conformityRate >= 100 ? 'conforme' : conformityRate >= 70 ? 'partiel' : 'non-conforme'\n });\n \n // Ajouter \u00e0 la conformit\u00e9 globale\n globalConformity += conformityRate;\n}\n\n// Moyenne de conformit\u00e9 globale\nglobalConformity = Math.round(globalConformity / indicators.length);\n\n// Trier par priorit\u00e9 (non-conformes en premier)\nconformityReport.sort((a, b) => a.conformityRate - b.conformityRate);\n\nreturn [\n {\n json: {\n reportDate: new Date().toISOString(),\n globalConformity,\n globalStatus: globalConformity >= 90 ? 'conforme' : globalConformity >= 70 ? '\u00e0 am\u00e9liorer' : 'critique',\n detailedReport: conformityReport\n }\n }\n];"
},
"name": "Analyse de Conformit\u00e9",
"type": "n8n-nodes-base.function",
"typeVersion": 1,
"position": [
-700,
140
],
"id": "3c6c5977-0899-452c-a854-106299bd119d"
},
{
"parameters": {
"application": {
"__rl": true,
"mode": "url",
"value": ""
},
"table": "Rapports Mensuels"
},
"name": "Enregistrer Rapport",
"type": "n8n-nodes-base.airtable",
"typeVersion": 1,
"position": [
-500,
140
],
"id": "4a4f5af0-c425-4892-af93-2212c8db3ea8"
},
{
"parameters": {
"conditions": {
"number": [
{
"value1": "={{$json.globalConformity}}",
"value2": 80
}
]
}
},
"name": "V\u00e9rifier Seuil d'Alerte",
"type": "n8n-nodes-base.if",
"typeVersion": 1,
"position": [
20,
-40
],
"id": "a205b93e-4130-4a79-935d-5c628364ccc5"
},
{
"parameters": {
"subject": "\u26a0\ufe0f ALERTE QUALIOPI - Taux de conformit\u00e9 : {{$json.globalConformity}}%",
"text": "=Rapport de conformit\u00e9 Qualiopi du {{$json.reportDate.split('T')[0]}}\n\nStatut global : {{$json.globalStatus.toUpperCase()}} ({{$json.globalConformity}}%)\n\nIndicateurs non conformes \u00e0 traiter en priorit\u00e9 :\n{% for item in $json.detailedReport %}{% if item.status === 'non-conforme' %}\n- {{item.indicatorCode}} ({{item.conformityRate}}%) : {{item.indicatorName}}\n Documents manquants : {{ item.requiredDocs.filter(doc => !item.associatedDocs.includes(doc)).join(', ') }}\n{% endif %}{% endfor %}\n\nVeuillez prendre les mesures n\u00e9cessaires pour restaurer la conformit\u00e9 avant le prochain audit.",
"options": {}
},
"name": "Envoi Alerte Conformit\u00e9",
"type": "n8n-nodes-base.emailSend",
"typeVersion": 1,
"position": [
240,
-60
],
"id": "69906582-dd01-45f2-b4cc-ba3b08c09d0d"
},
{
"parameters": {
"rule": {
"interval": [
{
"field": "weeks"
}
]
}
},
"name": "D\u00e9clencheur Hebdomadaire",
"type": "n8n-nodes-base.scheduleTrigger",
"typeVersion": 1,
"position": [
-1300,
340
],
"id": "e0b4c4e4-42d1-444e-9015-73689d4cc31e"
},
{
"parameters": {
"application": {
"__rl": true,
"mode": "url",
"value": ""
},
"table": "Formateurs"
},
"name": "Liste Formateurs",
"type": "n8n-nodes-base.airtable",
"typeVersion": 1,
"position": [
-1100,
340
],
"id": "f988e7c6-49b0-442a-b441-c6982bc95644"
},
{
"parameters": {
"functionCode": "// V\u00e9rifier les certifications des formateurs qui expirent bient\u00f4t\nconst formateurs = items;\nconst today = new Date();\nconst alertFormateurs = [];\n\nfor (const formateur of formateurs) {\n const certifications = formateur.json.certifications || [];\n const alertCertifications = [];\n \n for (const cert of certifications) {\n if (cert.expirationDate) {\n const expDate = new Date(cert.expirationDate);\n const diffTime = expDate - today;\n const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24));\n \n if (diffDays <= 60 && diffDays > 0) {\n alertCertifications.push({\n ...cert,\n daysUntilExpiration: diffDays\n });\n }\n }\n }\n \n if (alertCertifications.length > 0) {\n alertFormateurs.push({\n json: {\n formateurId: formateur.json.id,\n nom: formateur.json.nom,\n prenom: formateur.json.prenom,\n email: formateur.json.email,\n certifications: alertCertifications\n }\n });\n }\n}\n\nreturn alertFormateurs;"
},
"name": "V\u00e9rifier Certifications",
"type": "n8n-nodes-base.function",
"typeVersion": 1,
"position": [
-900,
340
],
"id": "01ca9370-a585-45f3-a08f-eb14d2c3364c"
},
{
"parameters": {
"batchSize": 1,
"options": {}
},
"name": "Traiter Par Formateur",
"type": "n8n-nodes-base.splitInBatches",
"typeVersion": 1,
"position": [
-700,
340
],
"id": "0cf116e5-cc08-47b6-808d-8ca28f911236"
},
{
"parameters": {
"subject": "Qualiopi - Alerte expiration de certification(s)",
"text": "=Bonjour {{$json.prenom}},\n\nNous vous informons que les certifications suivantes arrivent \u00e0 expiration :\n\n{% for cert in $json.certifications %}\n- {{cert.nom}} : expire dans {{cert.daysUntilExpiration}} jours ({{cert.expirationDate.split('T')[0]}})\n{% endfor %}\n\nAfin de maintenir notre certification Qualiopi, merci de pr\u00e9voir le renouvellement de ces qualifications.\n\nCordialement,\nService Qualit\u00e9",
"options": {}
},
"name": "Alerte Expiration Certification",
"type": "n8n-nodes-base.emailSend",
"typeVersion": 1,
"position": [
-500,
340
],
"id": "5528d62f-4944-4351-8f55-5e8306abcbd9"
},
{
"parameters": {
"url": "={{$env.VEILLE_QUALIOPI_URL}}",
"options": {
"fullResponse": true
}
},
"name": "Scraper Veille R\u00e9glementaire",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 1,
"position": [
-1100,
540
],
"id": "4328189d-7c3e-42de-a750-c12c6e77eab7"
},
{
"parameters": {
"functionCode": "// Analyse des changements r\u00e9glementaires\nconst response = items[0].json;\nconst previousData = $getWorkflowStaticData('node', 'previousContent') || '';\nconst currentContent = response.body;\n\n// Si pas de contenu pr\u00e9c\u00e9dent, sauvegarder et sortir\nif (!previousData) {\n $setWorkflowStaticData('node', 'previousContent', currentContent);\n return [];\n}\n\n// D\u00e9tecter les changements\nif (previousData === currentContent) {\n // Aucun changement\n return [];\n}\n\n// Simuler une analyse IA des changements\nconst changes = {\n detectedDate: new Date().toISOString(),\n source: 'Veille automatique',\n changesDetected: true,\n summary: 'Nouvelles informations d\u00e9tect\u00e9es sur la r\u00e9glementation Qualiopi',\n contentSnippet: currentContent.substring(0, 500) + '...',\n importance: 'moyenne',\n affectedCriteria: ['Crit\u00e8re 6', 'Crit\u00e8re 7']\n};\n\n// Sauvegarder le nouveau contenu\n$setWorkflowStaticData('node', 'previousContent', currentContent);\n\nreturn [{ json: changes }];"
},
"name": "Analyser Changements",
"type": "n8n-nodes-base.function",
"typeVersion": 1,
"position": [
-900,
540
],
"id": "7d18d7c0-0120-44bb-93af-2d79645ce3ca"
},
{
"parameters": {
"conditions": {
"boolean": [
{
"value1": "={{$json.changesDetected}}",
"value2": true
}
]
}
},
"name": "Si Changements D\u00e9tect\u00e9s",
"type": "n8n-nodes-base.if",
"typeVersion": 1,
"position": [
-700,
540
],
"id": "9efe4807-8873-47d6-a4e6-1341a5471f7b"
},
{
"parameters": {
"application": {
"__rl": true,
"mode": "url",
"value": ""
},
"table": "Veille R\u00e9glementaire"
},
"name": "Enregistrer Veille",
"type": "n8n-nodes-base.airtable",
"typeVersion": 1,
"position": [
-500,
460
],
"id": "95419556-b123-441f-b8a3-2de91158bf0e"
},
{
"parameters": {
"chatId": "={{$env.QUALIOPI_CHAT_GROUP}}",
"text": "\ud83d\udd14 *ALERTE VEILLE R\u00c9GLEMENTAIRE*\n\n{{$json.summary}}\n\nCrit\u00e8res concern\u00e9s : {{$json.affectedCriteria.join(', ')}}\nImportance : {{$json.importance}}\n\nExtrait : {{$json.contentSnippet.substring(0, 200)}}...",
"additionalFields": {}
},
"name": "Notification Veille",
"type": "n8n-nodes-base.telegram",
"typeVersion": 1,
"position": [
-500,
620
],
"id": "5fd33602-5235-470d-b375-4707feb7dbfa"
},
{
"parameters": {
"path": "formation-creation",
"options": {}
},
"name": "D\u00e9clencheur Nouvelle Formation",
"type": "n8n-nodes-base.webhook",
"typeVersion": 1,
"position": [
-1300,
740
],
"id": "ba826226-44f8-4c62-b229-10ac2f752dad"
},
{
"parameters": {
"functionCode": "// Pr\u00e9paration des donn\u00e9es de formation pour g\u00e9n\u00e9ration des documents\nconst sessionData = items[0].json;\n\n// Structurer les donn\u00e9es pour les templates de documents\nconst formationData = {\n id: sessionData.id,\n titre: sessionData.titre,\n dateDebut: new Date(sessionData.dateDebut).toLocaleDateString('fr-FR'),\n dateFin: new Date(sessionData.dateFin).toLocaleDateString('fr-FR'),\n duree: sessionData.duree,\n lieu: sessionData.lieu,\n formateur: sessionData.formateur,\n objectifs: sessionData.objectifs,\n participants: sessionData.participants || [],\n prix: sessionData.prix,\n programme: sessionData.programme,\n prerequis: sessionData.prerequis,\n modalitesEvaluation: sessionData.modalitesEvaluation,\n client: sessionData.client,\n dateGeneration: new Date().toLocaleDateString('fr-FR')\n};\n\nreturn [{ json: formationData }];"
},
"name": "Pr\u00e9parer Donn\u00e9es Formation",
"type": "n8n-nodes-base.function",
"typeVersion": 1,
"position": [
-1100,
740
],
"id": "07d91c12-5325-4d93-8ac6-9cd307255be8"
},
{
"parameters": {},
"name": "G\u00e9n\u00e9rer Programme",
"type": "n8n-nodes-base.documint",
"typeVersion": 1,
"position": [
-900,
640
],
"id": "e80fdb25-9053-42c6-a1cd-3d2f4110dd71",
"credentials": {}
},
{
"parameters": {},
"name": "G\u00e9n\u00e9rer Convention",
"type": "n8n-nodes-base.documint",
"typeVersion": 1,
"position": [
-900,
740
],
"id": "182b6d08-dbad-4c52-822f-d2b88607029d",
"credentials": {}
},
{
"parameters": {},
"name": "G\u00e9n\u00e9rer Attestation",
"type": "n8n-nodes-base.documint",
"typeVersion": 1,
"position": [
-900,
840
],
"id": "94d77927-0be2-480e-8fdd-ecabf3c20a50",
"credentials": {}
},
{
"parameters": {
"functionCode": "// Combinaison des r\u00e9sultats des documents g\u00e9n\u00e9r\u00e9s\nconst formationData = items[0].json;\nconst programmeDoc = items[1].json;\nconst conventionDoc = items[2].json;\nconst attestationDoc = items[3].json;\n\nreturn [{\n json: {\n formationId: formationData.id,\n formationTitre: formationData.titre,\n dateGeneration: new Date().toISOString(),\n documents: {\n programme: programmeDoc.url || programmeDoc.fileContent,\n convention: conventionDoc.url || conventionDoc.fileContent,\n attestation: attestationDoc.url || attestationDoc.fileContent,\n },\n participants: formationData.participants,\n formateur: formationData.formateur,\n client: formationData.client\n }\n}];"
},
"name": "Regrouper Documents",
"type": "n8n-nodes-base.function",
"typeVersion": 1,
"position": [
-700,
740
],
"id": "299c0bd2-527d-4e32-a858-ea5c8ec73e50"
},
{
"parameters": {
"application": {
"__rl": true,
"mode": "url",
"value": ""
},
"table": "Documents Formations"
},
"name": "Enregistrer Documents",
"type": "n8n-nodes-base.airtable",
"typeVersion": 1,
"position": [
-500,
740
],
"id": "d1b7be14-b24b-4e03-8485-45fab219234d"
},
{
"parameters": {},
"name": "Demande Signatures",
"type": "n8n-nodes-base.signNow",
"typeVersion": 1,
"position": [
-300,
740
],
"id": "8f51b0bb-e6bc-4ef2-9b41-fa9b99893c0f",
"credentials": {}
},
{
"parameters": {
"path": "formation-completed",
"options": {}
},
"name": "D\u00e9clencheur Formation Termin\u00e9e",
"type": "n8n-nodes-base.webhook",
"typeVersion": 1,
"position": [
-1300,
940
],
"id": "21b0ffaa-8781-4b2c-b226-cd5940f63cf7"
},
{
"parameters": {
"functionCode": "// Extraction des donn\u00e9es de formation compl\u00e9t\u00e9e\nconst formationData = items[0].json;\nconst participants = formationData.participants || [];\n\n// Cr\u00e9er un \u00e9l\u00e9ment par participant pour les \u00e9valuations\nconst participantsItems = [];\n\nfor (const participant of participants) {\n participantsItems.push({\n json: {\n formationId: formationData.id,\n formationTitre: formationData.titre,\n dateFin: formationData.dateFin,\n participant: participant,\n formateur: formationData.formateur,\n evaluationLink: `https://forms.example.com/evaluation?session=${formationData.id}&participant=${participant.id}`\n }\n });\n}\n\nreturn participantsItems;"
},
"name": "Pr\u00e9parer \u00c9valuations",
"type": "n8n-nodes-base.function",
"typeVersion": 1,
"position": [
-1100,
940
],
"id": "f41d5ced-7c58-43e3-b823-8f2dbe2e8dd4"
},
{
"parameters": {
"batchSize": 1,
"options": {}
},
"name": "Par Participant",
"type": "n8n-nodes-base.splitInBatches",
"typeVersion": 1,
"position": [
-900,
940
],
"id": "0640ea41-9a61-4390-8c32-aa04631df1d4"
},
{
"parameters": {
"subject": "\u00c9valuation de la formation \"{{$json.formationTitre}}\"",
"text": "=Bonjour {{$json.participant.prenom}},\n\nMerci d'avoir particip\u00e9 \u00e0 notre formation \"{{$json.formationTitre}}\".\n\nDans le cadre de notre d\u00e9marche qualit\u00e9 Qualiopi, nous vous invitons \u00e0 compl\u00e9ter l'\u00e9valuation \u00e0 chaud de cette session en cliquant sur le lien suivant :\n\n{{$json.evaluationLink}}\n\nVotre retour nous est pr\u00e9cieux pour am\u00e9liorer continuellement nos formations.\n\nCordialement,\nL'\u00e9quipe p\u00e9dagogique",
"options": {}
},
"name": "Envoyer Questionnaire \u00c9valuation",
"type": "n8n-nodes-base.emailSend",
"typeVersion": 1,
"position": [
-700,
940
],
"id": "228417d3-0da7-40c2-9df5-68a6546d9139"
},
{
"parameters": {
"application": {
"__rl": true,
"mode": "url",
"value": ""
},
"table": "Suivi \u00c9valuations"
},
"name": "Enregistrer Envoi \u00c9valuation",
"type": "n8n-nodes-base.airtable",
"typeVersion": 1,
"position": [
-500,
940
],
"id": "fca617ca-3b4c-418b-b4cb-92fd736c6dc9"
},
{
"parameters": {
"path": "evaluation-received",
"options": {}
},
"name": "R\u00e9ception \u00c9valuation",
"type": "n8n-nodes-base.webhook",
"typeVersion": 1,
"position": [
-1300,
1140
],
"id": "78420ff2-584f-4135-a810-e546883a6e56"
},
{
"parameters": {
"functionCode": "// Traitement de l'\u00e9valuation re\u00e7ue\nconst evaluationData = items[0].json;\n\n// Calculer un score global\nconst scores = evaluationData.scores || {};\nlet totalScore = 0;\nlet count = 0;\n\nfor (const key in scores) {\n if (typeof scores[key] === 'number') {\n totalScore += scores[key];\n count++;\n }\n}\n\nconst averageScore = count > 0 ? Math.round((totalScore / count) * 10) / 10 : 0;\n\n// Identifier les points d'am\u00e9lioration\nconst lowScoreThreshold = 3;\nconst improvementAreas = [];\n\nfor (const key in scores) {\n if (typeof scores[key] === 'number' && scores[key] <= lowScoreThreshold) {\n improvementAreas.push(key);\n }\n}\n\n// Pr\u00e9parer le r\u00e9sum\u00e9\nreturn [{\n json: {\n ...evaluationData,\n averageScore,\n improvementAreas,\n status: averageScore >= 4 ? 'satisfaisant' : averageScore >= 3 ? 'moyen' : 'insatisfaisant',\n dateReception: new Date().toISOString(),\n commentaireAnalyse: improvementAreas.length > 0 \n ? `Points \u00e0 am\u00e9liorer : ${improvementAreas.join(', ')}` \n : 'Aucun point critique identifi\u00e9'\n }\n}];"
},
"name": "Analyser \u00c9valuation",
"type": "n8n-nodes-base.function",
"typeVersion": 1,
"position": [
-1100,
1140
],
"id": "5cb90d1a-6d51-43a0-9ef6-c506ed4935d4"
},
{
"parameters": {
"application": {
"__rl": true,
"mode": "url",
"value": ""
},
"table": "R\u00e9sultats \u00c9valuations"
},
"name": "Enregistrer R\u00e9sultats",
"type": "n8n-nodes-base.airtable",
"typeVersion": 1,
"position": [
-900,
1140
],
"id": "5726e624-3079-4259-b4bc-1c7f6a8ef936"
},
{
"parameters": {
"conditions": {
"string": [
{
"value1": "={{$json.status}}",
"operation": "equals",
"value2": "insatisfaisant"
}
]
}
},
"name": "V\u00e9rifier \u00c9valuation Insatisfaisante",
"type": "n8n-nodes-base.if",
"typeVersion": 1,
"position": [
-700,
1140
],
"id": "3bae5c39-a142-4cbd-a7e8-29018dcd22d8"
},
{
"parameters": {
"authentication": "airtableTokenApi",
"application": {
"__rl": true,
"mode": "url",
"value": ""
},
"table": "Actions Correctives"
},
"name": "Cr\u00e9er Action Corrective",
"type": "n8n-nodes-base.airtable",
"typeVersion": 1,
"position": [
-500,
1040
],
"id": "c4e46022-3019-4dee-82e4-95af77650f95",
"credentials": {
"airtableTokenApi": {
"name": "<your credential>"
}
}
},
{
"parameters": {
"subject": "\u26a0\ufe0f Alerte Qualiopi - \u00c9valuation insatisfaisante",
"text": "=Alerte qualit\u00e9 formation\n\nUne \u00e9valuation insatisfaisante a \u00e9t\u00e9 re\u00e7ue pour la formation \"{{$json.formationTitre}}\".\n\nScore moyen : {{$json.averageScore}}/5\n\nPoints d'am\u00e9lioration identifi\u00e9s :\n{% for area in $json.improvementAreas %}- {{area}}\n{% endfor %}\n\nCommentaires du participant :\n{{$json.commentaire}}\n\nUne action corrective a \u00e9t\u00e9 automatiquement cr\u00e9\u00e9e dans le syst\u00e8me.\nMerci de la traiter conform\u00e9ment \u00e0 notre proc\u00e9dure qualit\u00e9 Qualiopi.",
"options": {}
},
"name": "Notifier Responsable Qualit\u00e9",
"type": "n8n-nodes-base.emailSend",
"typeVersion": 1,
"position": [
-300,
1040
],
"id": "f6a4b3e0-59d0-47d4-aa8f-3302524c90d3"
},
{
"parameters": {
"rule": {
"interval": [
{
"field": "months"
}
]
}
},
"name": "G\u00e9n\u00e9ration Rapports Mensuels",
"type": "n8n-nodes-base.scheduleTrigger",
"typeVersion": 1,
"position": [
-1300,
1340
],
"id": "39fe0936-70f2-4bc0-ac1e-96157441381d"
},
{
"parameters": {
"application": {
"__rl": true,
"mode": "url",
"value": ""
},
"table": "R\u00e9sultats \u00c9valuations"
},
"name": "R\u00e9cup\u00e9rer \u00c9valuations R\u00e9centes",
"type": "n8n-nodes-base.airtable",
"typeVersion": 1,
"position": [
-1100,
1340
],
"id": "c688adb7-dd38-4470-acf9-d97a31488ac1"
},
{
"parameters": {
"functionCode": "// G\u00e9n\u00e9rer le rapport mensuel d'\u00e9valuation\nconst evaluations = items[0].json.data;\nconst now = new Date();\nconst monthAgo = new Date();\nmonthAgo.setMonth(now.getMonth() - 1);\n\n// Filtrer les \u00e9valuations du dernier mois\nconst recentEvaluations = evaluations.filter(eval => {\n const evalDate = new Date(eval.fields.dateReception);\n return evalDate >= monthAgo && evalDate <= now;\n});\n\n// Calculer les statistiques\nlet totalScore = 0;\nconst formationScores = {};\nconst formateurScores = {};\nconst improvementAreasCount = {};\n\nrecentEvaluations.forEach(eval => {\n // Score moyen global\n totalScore += eval.fields.averageScore || 0;\n \n // Scores par formation\n const formationId = eval.fields.formationId;\n if (!formationScores[formationId]) {\n formationScores[formationId] = {\n titre: eval.fields.formationTitre,\n scores: [],\n total: 0,\n count: 0\n };\n }\n formationScores[formationId].scores.push(eval.fields.averageScore);\n formationScores[formationId].total += eval.fields.averageScore;\n formationScores[formationId].count++;\n \n // Scores par formateur\n const formateurId = eval.fields.formateurId;\n if (!formateurScores[formateurId]) {\n formateurScores[formateurId] = {\n nom: eval.fields.formateurNom,\n scores: [],\n total: 0,\n count: 0\n };\n }\n formateurScores[formateurId].scores.push(eval.fields.averageScore);\n formateurScores[formateurId].total += eval.fields.averageScore;\n formateurScores[formateurId].count++;\n \n // Points d'am\u00e9lioration\n (eval.fields.improvementAreas || []).forEach(area => {\n if (!improvementAreasCount[area]) {\n improvementAreasCount[area] = 0;\n }\n improvementAreasCount[area]++;\n });\n});\n\n// Calcul des moyennes\nconst globalAverage = recentEvaluations.length > 0 ? Math.round((totalScore / recentEvaluations.length) * 10) / 10 : 0;\n\n// Classement des formations par score\nconst formationRanking = Object.keys(formationScores)\n .map(id => ({\n id,\n titre: formationScores[id].titre,\n average: formationScores[id].count > 0 ? Math.round((formationScores[id].total / formationScores[id].count) * 10) / 10 : 0\n }))\n .sort((a, b) => b.average - a.average);\n\n// Classement des formateurs par score\nconst formateurRanking = Object.keys(formateurScores)\n .map(id => ({\n id,\n nom: formateurScores[id].nom,\n average: formateurScores[id].count > 0 ? Math.round((formateurScores[id].total / formateurScores[id].count) * 10) / 10 : 0\n }))\n .sort((a, b) => b.average - a.average);\n\n// Points d'am\u00e9lioration les plus fr\u00e9quents\nconst topImprovementAreas = Object.keys(improvementAreasCount)\n .map(area => ({ area, count: improvementAreasCount[area] }))\n .sort((a, b) => b.count - a.count)\n .slice(0, 5);\n\n// Rapport final\nreturn [{\n json: {\n periode: `${monthAgo.toLocaleDateString()} - ${now.toLocaleDateString()}`,\n dateGeneration: now.toISOString(),\n nombreEvaluations: recentEvaluations.length,\n scoreMoyenGlobal: globalAverage,\n tendance: 0, // \u00c0 calculer avec l'historique\n meilleuresFormations: formationRanking.slice(0, 3),\n formationsAmeliorer: formationRanking.slice(-3).reverse(),\n meilleursFormateurs: formateurRanking.slice(0, 3),\n pointsAmelioration: topImprovementAreas,\n qualiopiStatus: globalAverage >= 4 ? 'conforme' : globalAverage >= 3.5 ? '\u00e0 surveiller' : 'non-conforme'\n }\n}];"
},
"name": "G\u00e9n\u00e9rer Rapport Mensuel",
"type": "n8n-nodes-base.function",
"typeVersion": 1,
"position": [
-900,
1340
],
"id": "f4a356b0-cc71-481d-ac32-09da600d6ab5"
},
{
"parameters": {},
"name": "G\u00e9n\u00e9rer PDF Rapport",
"type": "n8n-nodes-base.documint",
"typeVersion": 1,
"position": [
-700,
1340
],
"id": "47a645b4-aaef-475e-ba70-8205c94d2cc6",
"credentials": {}
},
{
"parameters": {
"subject": "Rapport mensuel Qualiopi - {{$json.periode}}",
"text": "=Bonjour,\n\nVeuillez trouver ci-joint le rapport mensuel de conformit\u00e9 Qualiopi pour la p\u00e9riode {{$json.periode}}.\n\nPoints cl\u00e9s :\n- Score moyen global : {{$json.scoreMoyenGlobal}}/5\n- Nombre d'\u00e9valuations trait\u00e9es : {{$json.nombreEvaluations}}\n- Statut Qualiopi : {{$json.qualiopiStatus}}\n\nFormations les mieux \u00e9valu\u00e9es :\n{% for formation in $json.meilleuresFormations %}- {{formation.titre}} ({{formation.average}}/5)\n{% endfor %}\n\nPoints d'am\u00e9lioration principaux :\n{% for point in $json.pointsAmelioration %}- {{point.area}} (mentionn\u00e9 {{point.count}} fois)\n{% endfor %}\n\nCordialement,\nSyst\u00e8me d'assurance qualit\u00e9 Qualiopi",
"options": {}
},
"name": "Diffuser Rapport",
"type": "n8n-nodes-base.emailSend",
"typeVersion": 1,
"position": [
-300,
140
],
"id": "f7794e01-f004-4e26-a52d-4df6413de396"
},
{
"parameters": {},
"type": "n8n-nodes-base.manualTrigger",
"typeVersion": 1,
"position": [
-1540,
840
],
"id": "51e04c82-1bfe-4530-b262-8def22256bdd",
"name": "When clicking \u2018Test workflow\u2019"
}
],
"settings": {
"executionOrder": "v1"
},
"staticData": null,
"tags": [],
"triggerCount": 0,
"updatedAt": "2025-05-22T12:50:53.000Z",
"versionId": "cd685b54-4b43-41c9-b572-3e7fca58875c"
}
Credentials you'll need
Each integration node will prompt for credentials when you import. We strip credential IDs before publishing — you'll add your own.
airtableTokenApi
For the full experience including quality scoring and batch install features for each workflow upgrade to Pro
About this workflow
qualiopi. Uses airtable, telegram, emailSend, httpRequest. Webhook trigger; 51 nodes.
Source: https://github.com/Festen78/N8N-Backup/blob/2133d945109be62c0516644be8c2dee3a67327e6/workflows/p4n8rgT4RWrRAj6a.json — original creator credit. Request a take-down →
Related workflows
Workflows that share integrations, category, or trigger type with this one. All free to copy and import.
How it works • Webhook triggers from content creation system in Airtable • Downloads media (images/videos) from Airtable URLs • Uploads media to Postiz cloud storage • Schedules or publishes content a
This workflow contains community nodes that are only compatible with the self-hosted version of n8n.
Overview
My workflow 4. Uses httpRequest, emailSend, telegram. Webhook trigger; 6 nodes.
This n8n workflow collects client feedback through a form (Tally, Typeform, or Google Forms) and uses AI to analyze it. It automatically generates a summary of the positive points, highlights areas fo