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 →
{
"nodes": [
{
"parameters": {
"httpMethod": "POST",
"path": "teaching-genome/generate-pdf",
"responseMode": "responseNode",
"options": {}
},
"id": "webhook-pdf",
"name": "Webhook - Generate PDF",
"position": [
-352,
0
],
"type": "n8n-nodes-base.webhook",
"typeVersion": 2.1
},
{
"parameters": {
"operation": "get",
"tableId": "weeks",
"filters": {
"conditions": [
{
"keyName": "course_id",
"keyValue": "={{ $json.body.course_id }}"
},
{
"keyName": "week_number",
"keyValue": "={{ $json.body.week_number }}"
}
]
}
},
"id": "fetch-week",
"name": "Fetch Week Data",
"position": [
0,
0
],
"type": "n8n-nodes-base.supabase",
"typeVersion": 1,
"credentials": {
"supabaseApi": {
"name": "<your credential>"
}
}
},
{
"parameters": {
"modelId": {
"__rl": true,
"cachedResultName": "models/gemini-2.5-flash",
"mode": "list",
"value": "models/gemini-2.5-flash"
},
"messages": {
"values": [
{
"content": "=You are a university professor. Create detailed lecture slide content for this week.\nWeek: {{ $json.week_number }}\nTopic: {{ $json.topic }}\nLearning Objectives: {{ Array.isArray($json.learning_objectives) ? $json.learning_objectives.join(', ') : $json.learning_objectives }}\n\nCRITICAL FORMATTING RULES:\n1. Use SECTION: as the ONLY header format. Example: SECTION: Introduction\n2. Do NOT use markdown (no ###, no **, no *, no ```)\n3. Do NOT use LaTeX math notation\n4. Do NOT use tables\n5. Use plain text bullet points with dash prefix: - bullet point\n6. Separate sections clearly with blank lines\n7. Write 8-12 sections\n8. Each section should have 3-6 bullet points or 2-3 paragraphs of plain text\n\nSTRUCTURE:\nSECTION: Title\nContent: Week {{ $json.week_number }}: {{ $json.topic }}\n\nSECTION: Learning Objectives\n{{ Array.isArray($json.learning_objectives) ? $json.learning_objectives.map(o => '- ' + o).join('\\n') : '- ' + $json.learning_objectives }}\n\nSECTION: Introduction\n[2-3 paragraphs introducing the topic]\n\nSECTION: [Key Concept 1]\n[2-3 paragraphs or 3-6 bullet points]\n\nSECTION: [Key Concept 2]\n[2-3 paragraphs or 3-6 bullet points]\n\nSECTION: [Key Concept 3]\n[2-3 paragraphs or 3-6 bullet points]\n\nSECTION: Real-World Applications\n- Application 1 with explanation\n- Application 2 with explanation\n- Application 3 with explanation\n\nSECTION: Discussion Points\n- Discussion question 1\n- Discussion question 2\n- Discussion question 3\n\nSECTION: Summary\n[2-3 paragraphs summarizing key takeaways]\n\nMake it comprehensive, lecturer-ready, and plain text only."
}
]
},
"builtInTools": {},
"options": {}
},
"id": "a3694f17-f025-4f09-b50d-e4a2e6bb1bc5",
"name": "genrate pdf",
"position": [
272,
0
],
"type": "@n8n/n8n-nodes-langchain.googleGemini",
"typeVersion": 1.2,
"credentials": {
"googlePalmApi": {
"name": "<your credential>"
}
}
},
{
"parameters": {
"jsCode": "var gemini = items[0].json;\nvar week = $('Fetch Week Data').first().json;\n\nvar content = (gemini.content && gemini.content.parts && gemini.content.parts[0] && gemini.content.parts[0].text)\n || (gemini.parts && gemini.parts[0] && gemini.parts[0].text)\n || gemini.text\n || JSON.stringify(gemini);\n\nif (typeof content !== 'string') content = JSON.stringify(content);\ncontent = content.replace(/\\\\n/g, '\\n');\n\nvar weekNum = week.week_number || 1;\nvar topic = String(week.topic || 'Untitled');\nvar teachingNotes = String(week.teaching_notes || '');\n\nfunction parseArr(d) {\n if (!d) return [];\n if (Array.isArray(d)) return d.map(String);\n if (typeof d === 'string') {\n try {\n var p = JSON.parse(d);\n return Array.isArray(p) ? p.map(String) : [d];\n } catch(e) { return [d]; }\n }\n return [];\n}\n\nvar objectives = parseArr(week.learning_objectives);\nvar prompts = parseArr(week.discussion_prompts);\nvar activities = parseArr(week.activity_ideas);\n\nfunction esc(s) {\n return String(s || '').replace(/[\\\\()]/g, function(m) { return '\\\\' + m; }).replace(/[^\\x20-\\x7E]/g, '');\n}\n\nfunction wrap(text, maxLen) {\n var words = String(text).split(' ');\n var lines = [];\n var line = '';\n for (var i = 0; i < words.length; i++) {\n if ((line + words[i]).length > maxLen && line) {\n lines.push(line.trim());\n line = words[i] + ' ';\n } else {\n line += words[i] + ' ';\n }\n }\n if (line.trim()) lines.push(line.trim());\n return lines;\n}\n\nvar BLUE = '0.0 0.4 0.8';\nvar DARK = '0.13 0.13 0.16';\nvar MID = '0.35 0.35 0.4';\nvar GREEN = '0.15 0.68 0.38';\nvar ORANGE = '0.9 0.5 0.13';\nvar PURPLE = '0.56 0.27 0.68';\nvar LINE_C = '0.85 0.85 0.88';\n\nvar pages = [];\n\nfunction newPage() {\n var p = { cmds: [], y: 790 };\n pages.push(p);\n return p;\n}\n\nfunction addFooter(p) {\n p.cmds.push(LINE_C + ' RG 0.3 w 50 42 m 545 42 l S');\n p.cmds.push('0.65 0.65 0.68 rg');\n p.cmds.push('BT /F1 7 Tf 50 30 Td (Teaching Genome | Week ' + weekNum + ' | Page ' + pages.length + ') Tj ET');\n}\n\nfunction addTextBlock(p, text, fs, color, indent) {\n fs = fs || 10;\n indent = indent || 50;\n var maxC = Math.floor((495 - (indent - 50)) / (fs * 0.48));\n var tLines = String(text).split('\\n');\n for (var t = 0; t < tLines.length; t++) {\n var wl = wrap(tLines[t], maxC);\n if (wl.length === 0) { p.y -= 8; continue; }\n for (var w = 0; w < wl.length; w++) {\n if (p.y < 60) { addFooter(p); p = newPage(); }\n p.cmds.push((color || DARK) + ' rg');\n p.cmds.push('BT /F1 ' + fs + ' Tf ' + indent + ' ' + p.y + ' Td (' + esc(wl[w]) + ') Tj ET');\n p.y -= (fs + 4);\n }\n }\n return p;\n}\n\nfunction addBullets(p, items, color) {\n for (var i = 0; i < items.length; i++) {\n if (p.y < 60) { addFooter(p); p = newPage(); }\n p.cmds.push((color || BLUE) + ' rg');\n p.cmds.push('50 ' + (p.y + 2) + ' 5 5 re f');\n var bl = wrap(items[i], 72);\n for (var b = 0; b < bl.length; b++) {\n if (p.y < 55) { addFooter(p); p = newPage(); }\n p.cmds.push(DARK + ' rg');\n p.cmds.push('BT /F1 10 Tf 62 ' + p.y + ' Td (' + esc(bl[b]) + ') Tj ET');\n p.y -= 14;\n }\n p.y -= 3;\n }\n return p;\n}\n\nfunction sectionTitle(p, title, color) {\n if (p.y < 120) { addFooter(p); p = newPage(); }\n p.cmds.push(LINE_C + ' RG 0.3 w 50 ' + p.y + ' m 545 ' + p.y + ' l S');\n p.y -= 22;\n p.cmds.push((color || BLUE) + ' rg');\n p.cmds.push('BT /F2 13 Tf 50 ' + p.y + ' Td (' + esc(title) + ') Tj ET');\n p.y -= 20;\n return p;\n}\n\n// PAGE 1: Title\nvar p = newPage();\np.cmds.push('0.0 0.3 0.65 rg');\np.cmds.push('0 750 595.28 91.89 re f');\np.cmds.push('1 1 1 rg');\np.cmds.push('BT /F2 28 Tf 50 780 Td (Week ' + weekNum + ') Tj ET');\np.cmds.push('0.7 0.85 1 rg');\np.cmds.push('BT /F1 14 Tf 50 758 Td (Teaching Genome) Tj ET');\np.y = 710;\np.cmds.push(DARK + ' rg');\np.cmds.push('BT /F2 20 Tf 50 ' + p.y + ' Td (' + esc(topic) + ') Tj ET');\np.y -= 10;\np.cmds.push(BLUE + ' RG 1 w 50 ' + p.y + ' m 200 ' + p.y + ' l S');\np.y -= 30;\n\nif (objectives.length > 0) { p = sectionTitle(p, 'LEARNING OBJECTIVES', GREEN); p = addBullets(p, objectives, GREEN); p.y -= 10; }\nif (prompts.length > 0) { p = sectionTitle(p, 'DISCUSSION TOPICS', BLUE); p = addBullets(p, prompts, BLUE); p.y -= 10; }\n\nif (activities.length > 0) {\n p = sectionTitle(p, 'ACTIVITIES', ORANGE);\n for (var a = 0; a < activities.length; a++) {\n if (p.y < 60) { addFooter(p); p = newPage(); }\n p.cmds.push(ORANGE + ' rg');\n p.cmds.push('BT /F2 10 Tf 50 ' + p.y + ' Td (' + (a + 1) + '.) Tj ET');\n var al = wrap(activities[a], 72);\n for (var x = 0; x < al.length; x++) {\n p.cmds.push(DARK + ' rg');\n p.cmds.push('BT /F1 10 Tf 70 ' + p.y + ' Td (' + esc(al[x]) + ') Tj ET');\n p.y -= 14;\n }\n p.y -= 4;\n }\n}\naddFooter(p);\n\n// GEMINI CONTENT PAGES\nvar contentLines = content.split('\\n');\nvar sections = [];\nvar curT = 'Lecture Content';\nvar curB = [];\n\nfor (var i = 0; i < contentLines.length; i++) {\n var ln = contentLines[i].trim();\n if (ln.match(/^SECTION:\\s+/) || ln.match(/^#{1,3}\\s+/) || ln.match(/^\\*\\*[^*]+\\*\\*$/)) {\n if (curB.length > 0) sections.push({ title: curT, body: curB.join('\\n').trim() });\n curT = ln.replace(/^SECTION:\\s+/, '').replace(/^#{1,3}\\s+/, '').replace(/^\\*\\*/, '').replace(/\\*\\*$/, '').trim();\n curB = [];\n } else if (ln.length > 0) {\n curB.push(ln);\n }\n}\nif (curB.length > 0) sections.push({ title: curT, body: curB.join('\\n').trim() });\nif (sections.length === 0) sections.push({ title: 'Lecture Content', body: content });\n\nvar colors = [BLUE, GREEN, PURPLE, ORANGE];\n\nfor (var s = 0; s < sections.length; s++) {\n var sec = sections[s];\n if (!sec.body.trim()) continue;\n p = newPage();\n var sc = colors[s % 4];\n p.cmds.push(sc + ' rg');\n p.cmds.push('0 800 595.28 41.89 re f');\n p.cmds.push('1 1 1 rg');\n p.cmds.push('BT /F2 18 Tf 50 812 Td (' + esc(sec.title) + ') Tj ET');\n p.y = 775;\n\n var bLines = sec.body.split('\\n');\n for (var b = 0; b < bLines.length; b++) {\n var bl = bLines[b].trim();\n if (!bl) { p.y -= 8; continue; }\n if (bl.match(/^[-\\*]\\s+/)) {\n var bt = bl.replace(/^[-\\*]\\s+/, '').replace(/^\\*\\*/, '').replace(/\\*\\*/g, '');\n if (p.y < 60) { addFooter(p); p = newPage(); }\n p.cmds.push(sc + ' rg');\n p.cmds.push('55 ' + (p.y + 2) + ' 4 4 re f');\n var bw = wrap(bt, 72);\n for (var w = 0; w < bw.length; w++) {\n if (p.y < 55) { addFooter(p); p = newPage(); }\n p.cmds.push(DARK + ' rg');\n p.cmds.push('BT /F1 10 Tf 66 ' + p.y + ' Td (' + esc(bw[w]) + ') Tj ET');\n p.y -= 14;\n }\n p.y -= 2;\n } else {\n var clean = bl.replace(/\\*\\*/g, '');\n p = addTextBlock(p, clean, 10, DARK, 50);\n p.y -= 2;\n }\n }\n addFooter(p);\n}\n\n// TEACHING NOTES PAGE\nif (teachingNotes) {\n p = newPage();\n p.cmds.push(PURPLE + ' rg');\n p.cmds.push('0 800 595.28 41.89 re f');\n p.cmds.push('1 1 1 rg');\n p.cmds.push('BT /F2 18 Tf 50 812 Td (Teaching Strategy) Tj ET');\n p.y = 775;\n p = addTextBlock(p, teachingNotes, 10, MID, 50);\n addFooter(p);\n}\n\n// ASSEMBLE PDF\nvar pageContents = [];\nfor (var i = 0; i < pages.length; i++) pageContents.push(pages[i].cmds.join('\\n'));\n\nvar pdf = '%PDF-1.4\\n';\nvar offsets = [];\n\nfunction addObj(ct) {\n offsets.push(pdf.length);\n pdf += ct + '\\n';\n}\n\naddObj('1 0 obj\\n<< /Type /Catalog /Pages 2 0 R >>\\nendobj');\n\nvar kids = [];\nfor (var i = 0; i < pages.length; i++) kids.push((3 + i * 2) + ' 0 R');\naddObj('2 0 obj\\n<< /Type /Pages /Kids [' + kids.join(' ') + '] /Count ' + pages.length + ' >>\\nendobj');\n\nvar f1 = 3 + pages.length * 2;\nvar f2 = f1 + 1;\n\nfor (var i = 0; i < pages.length; i++) {\n var pid = 3 + i * 2;\n var cid = 4 + i * 2;\n addObj(pid + ' 0 obj\\n<< /Type /Page /Parent 2 0 R /MediaBox [0 0 595.28 841.89] /Contents ' + cid + ' 0 R /Resources << /Font << /F1 ' + f1 + ' 0 R /F2 ' + f2 + ' 0 R >> >> >>\\nendobj');\n addObj(cid + ' 0 obj\\n<< /Length ' + pageContents[i].length + ' >>\\nstream\\n' + pageContents[i] + '\\nendstream\\nendobj');\n}\n\naddObj(f1 + ' 0 obj\\n<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>\\nendobj');\naddObj(f2 + ' 0 obj\\n<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica-Bold >>\\nendobj');\n\nvar total = f2 + 1;\nvar xref = pdf.length;\npdf += 'xref\\n0 ' + total + '\\n0000000000 65535 f \\n';\nfor (var i = 0; i < offsets.length; i++) pdf += String(offsets[i]).padStart(10, '0') + ' 00000 n \\n';\npdf += 'trailer\\n<< /Size ' + total + ' /Root 1 0 R >>\\nstartxref\\n' + xref + '\\n%%EOF';\n\nreturn [{ json: { pdfData: pdf, fileName: 'Week_' + weekNum + '_Slides.pdf' } }];"
},
"id": "format-pdf",
"name": "Build pdf",
"position": [
752,
0
],
"type": "n8n-nodes-base.code",
"typeVersion": 2
},
{
"parameters": {
"jsCode": "const pdfData = $input.first().json.pdfData;\nconst fileName = $input.first().json.fileName || 'slides.pdf';\nconst pdfBuffer = Buffer.from(pdfData, 'latin1');\nconst binaryData = await this.helpers.prepareBinaryData(pdfBuffer, fileName, 'application/pdf');\nreturn [{ json: { fileName }, binary: { data: binaryData } }];"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
1072,
-32
],
"id": "636a3801-c7b4-43c9-a3d1-532c544b63a9",
"name": "retun pdf"
},
{
"parameters": {
"respondWith": "binary",
"responseDataSource": "set",
"options": {}
},
"type": "n8n-nodes-base.respondToWebhook",
"typeVersion": 1.5,
"position": [
1264,
0
],
"id": "12350222-9e26-4bce-92b6-cb1810738f3a",
"name": "Respond to Webhook"
}
],
"connections": {
"Webhook - Generate PDF": {
"main": [
[
{
"index": 0,
"node": "Fetch Week Data",
"type": "main"
}
]
]
},
"Fetch Week Data": {
"main": [
[
{
"index": 0,
"node": "genrate pdf",
"type": "main"
}
]
]
},
"genrate pdf": {
"main": [
[
{
"index": 0,
"node": "Build pdf",
"type": "main"
}
]
]
},
"Build pdf": {
"main": [
[
{
"node": "retun pdf",
"type": "main",
"index": 0
}
]
]
},
"retun pdf": {
"main": [
[
{
"node": "Respond to Webhook",
"type": "main",
"index": 0
}
]
]
}
},
"meta": {
"templateCredsSetupCompleted": true
}
}
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.
googlePalmApisupabaseApi
For the full experience including quality scoring and batch install features for each workflow upgrade to Pro
About this workflow
Generate-Week-Pdf. Uses supabase, googleGemini. Webhook trigger; 6 nodes.
Source: https://github.com/anas318/teaching-genome/blob/faa7ef6d1c110d512b24855a7906e4ffe1e025d6/n8n-workflows/generate-week-pdf.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.
This workflow automates the process of generating stylized product photos for e-commerce by combining real product shots with creative templates. It enables the creation of a complete set of images fo
This workflow serves as a complete "AI Receptionist" for mortgage brokers or high-ticket service providers. It automates the messy process of qualifying leads, getting internal approval, and collectin
How it works Runs on schedule (Monday-Friday at 9 AM) to automate lead generation Searches for companies on Google Maps by location and category Extracts owner information from company websites and im
This workflow is designed for creators, designers, and automation builders who need to generate visually consistent images at scale. It is ideal for teams producing branded visuals, social media asset
FoodSnap - Unified (Food & Coach). Uses postgres, n8n-nodes-evolution-api, googleGemini. Webhook trigger; 22 nodes.