This workflow follows the HTTP Request → OpenAI 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 →
{
"name": "Main: Submit Assignment",
"nodes": [
{
"parameters": {
"httpMethod": "POST",
"path": "submit",
"options": {}
},
"id": "webhook-trigger",
"name": "Webhook Submit",
"type": "n8n-nodes-base.webhook",
"position": [
250,
300
],
"typeVersion": 1
},
{
"parameters": {
"jsCode": "const Database = require('better-sqlite3');\nconst db = new Database('e:/repos/golearn/data/curriculum.db');\n\nconst row = db.prepare('SELECT MAX(cycle_num) as max_cycle FROM cycles').get();\ndb.close();\n\nreturn {\n json: {\n max_cycle: row.max_cycle\n }\n};",
"mode": "runOnceForAllItems"
},
"id": "check-cycle",
"name": "Check Current Cycle",
"type": "n8n-nodes-base.code",
"position": [
450,
300
],
"typeVersion": 2
},
{
"parameters": {
"conditions": {
"boolean": [
{
"value1": "={{ $json.max_cycle }}",
"value2": true,
"operation": "isEmpty"
}
]
}
},
"id": "is-first-cycle",
"name": "Is First Cycle?",
"type": "n8n-nodes-base.if",
"position": [
650,
300
],
"typeVersion": 1
},
{
"parameters": {
"jsCode": "const Database = require('better-sqlite3');\nconst db = new Database('e:/repos/golearn/data/curriculum.db');\n\nconst bootstrapData = $node['Read Bootstrap'].json;\nconst curriculumJson = JSON.stringify(bootstrapData);\n\ndb.prepare('INSERT INTO cycles (cycle_num, curriculum_json, status) VALUES (?, ?, ?)').run(1, curriculumJson, 'active');\ndb.close();\n\nreturn {\n json: {\n success: true,\n curriculum: bootstrapData\n }\n};",
"mode": "runOnceForAllItems"
},
"id": "insert-bootstrap",
"name": "Insert Bootstrap Cycle",
"type": "n8n-nodes-base.code",
"position": [
850,
200
],
"typeVersion": 2
},
{
"parameters": {
"filePath": "e:/repos/golearn/scripts/bootstrap-curriculum.json",
"options": {}
},
"id": "read-bootstrap",
"name": "Read Bootstrap",
"type": "n8n-nodes-base.readBinaryFile",
"position": [
850,
100
],
"typeVersion": 1
},
{
"parameters": {
"jsCode": "const Database = require('better-sqlite3');\nconst db = new Database('e:/repos/golearn/data/curriculum.db');\n\nconst row = db.prepare('SELECT curriculum_json FROM cycles WHERE status=? ORDER BY id DESC LIMIT 1').get('active');\ndb.close();\n\nreturn {\n json: {\n curriculum_json: row ? row.curriculum_json : null\n }\n};",
"mode": "runOnceForAllItems"
},
"id": "get-current-curriculum",
"name": "Get Current Curriculum",
"type": "n8n-nodes-base.code",
"position": [
850,
400
],
"typeVersion": 2
},
{
"parameters": {
"jsCode": "// Extract concepts from current curriculum for assessment\nconst curriculum = JSON.parse($json.curriculum_json);\nconst allConcepts = [];\n\ncurriculum.weeks.forEach(week => {\n allConcepts.push(...week.key_concepts);\n});\n\nreturn {\n json: {\n ...$ input.first().json,\n current_concepts: allConcepts,\n curriculum: curriculum\n }\n};"
},
"id": "extract-concepts",
"name": "Extract Current Concepts",
"type": "n8n-nodes-base.code",
"position": [
1050,
400
],
"typeVersion": 2
},
{
"parameters": {
"jsCode": "// Parse GitHub URL and file paths\nconst webhook = $input.first().json.body;\nconst repoUrl = webhook.code_repo_url;\nconst filePaths = webhook.code_file_paths || 'main.go';\n\n// Extract owner and repo from URL\nconst match = repoUrl.match(/github\\.com\\/([^\\/]+)\\/([^\\/]+)/);\nif (!match) {\n throw new Error('Invalid GitHub URL format');\n}\n\nconst [, owner, repo] = match;\nconst paths = filePaths.split(',').map(p => p.trim());\n\nreturn paths.map(path => ({\n json: {\n owner,\n repo: repo.replace(/\\.git$/, ''),\n path,\n essay: webhook.essay_text,\n current_concepts: $input.first().json.current_concepts\n }\n}));"
},
"id": "parse-github-url",
"name": "Parse GitHub URL",
"type": "n8n-nodes-base.code",
"position": [
1250,
400
],
"typeVersion": 2
},
{
"parameters": {
"url": "=https://raw.githubusercontent.com/{{ $json.owner }}/{{ $json.repo }}/main/{{ $json.path }}",
"options": {}
},
"id": "fetch-code",
"name": "Fetch Code Files",
"type": "n8n-nodes-base.httpRequest",
"position": [
1450,
400
],
"typeVersion": 3
},
{
"parameters": {
"jsCode": "// Combine all code files with headers\nconst items = $input.all();\nlet combinedCode = '';\n\nitems.forEach(item => {\n const path = item.json.path;\n const content = typeof item.json.data === 'string' ? item.json.data : JSON.stringify(item.json.data);\n combinedCode += `// FILE: ${path}\\n${content}\\n\\n`;\n});\n\nreturn {\n json: {\n essay: items[0].json.essay,\n code: combinedCode,\n concepts: items[0].json.current_concepts\n }\n};"
},
"id": "combine-code",
"name": "Combine Code Files",
"type": "n8n-nodes-base.code",
"position": [
1650,
400
],
"typeVersion": 2
},
{
"parameters": {
"model": "gpt-4-turbo-preview",
"messages": {
"values": [
{
"role": "system",
"content": "=You are assessing a Go learning project.\n\nThe student was learning these EXACT concepts this cycle:\n{{ $json.concepts.join(', ') }}\n\nSCORING RUBRIC (return integers 1-5 ONLY):\n\n**Code Correctness**:\n1 = syntax errors/won't run\n2 = runs but broken logic/crashes\n3 = works for basic happy path cases\n4 = handles most errors and edge cases\n5 = comprehensive error handling with proper edge case coverage\n\n**Code Idioms**:\n1 = non-Go patterns (Java/Python style code)\n2 = compiles but poor naming/structure/style\n3 = mostly idiomatic Go with minor style issues\n4 = proper Go conventions and patterns throughout\n5 = exemplary Go style worthy of standard library\n\n**Code Completeness**:\n1 = missing >50% required features\n2 = partial implementation 50-75% done\n3 = all core features present and working\n4 = core features + unit tests present\n5 = features + comprehensive tests + documentation\n\nScore EACH concept from the list 1-5 based on evidence in code.\n\nSTRICT JSON OUTPUT:\n{\n \"essay_strengths\": [\"string array of what they explained well\"],\n \"essay_gaps\": [\"string array of missing/fuzzy concepts\"],\n \"code_correctness\": int,\n \"code_idioms\": int,\n \"code_completeness\": int,\n \"detailed_feedback\": \"string (2-3 paragraphs for student)\",\n \"concept_scores\": [\n {\n \"concept\": \"string (EXACT match from provided list)\",\n \"score\": int,\n \"evidence\": \"string\"\n }\n ]\n}"
},
{
"role": "user",
"content": "=# STUDENT ESSAY\n\n{{ $json.essay }}\n\n# STUDENT CODE\n\n{{ $json.code }}"
}
]
},
"options": {
"temperature": 0.4,
"maxTokens": 2000
},
"jsonOutput": true
},
"id": "assess-work",
"name": "Assess Essay and Code",
"type": "@n8n/n8n-nodes-langchain.openAi",
"position": [
1850,
400
],
"typeVersion": 1.3
},
{
"parameters": {
"jsCode": "// Adaptive concept selection algorithm\nconst Database = require('better-sqlite3');\nconst db = new Database('e:/repos/golearn/data/curriculum.db');\nconst fs = require('fs');\n\nconst assessment = JSON.parse($json.message.content);\nconst currentCycleId = $node[\"Check Current Cycle\"].json.max_cycle || 1;\n\n// 1. Gap concepts (score < 3) with weight 3\nconst gaps = assessment.concept_scores\n .filter(c => c.score < 3)\n .map(c => ({\n name: c.concept.toLowerCase().trim(),\n weight: 3,\n source: 'gap'\n }));\n\n// 2. Load topic buffet\nconst buffet = JSON.parse(fs.readFileSync('e:/repos/golearn/data/topic_buffet.json', 'utf8'));\n\n// 3. Get taught weeks\nconst taughtWeeks = db.prepare('SELECT DISTINCT week_id FROM taught_weeks').all().map(r => r.week_id);\n\n// 4. Calculate average score for a week\nfunction calcAvgScore(weekId) {\n const week = buffet.find(w => w.week_id === weekId);\n if (!week) return 0;\n \n const scores = week.concepts.map(c => {\n const row = db.prepare('SELECT current_score FROM concept_mastery WHERE concept_name = ?')\n .get(c.toLowerCase().trim());\n return row ? row.current_score : 0;\n });\n \n return scores.length > 0 ? scores.reduce((a,b) => a+b, 0) / scores.length : 0;\n}\n\n// 5. Find untaught weeks with prerequisites met\nconst untaughtWeeks = buffet\n .filter(w => !taughtWeeks.includes(w.week_id))\n .filter(w => w.prerequisite_week_ids.every(p => calcAvgScore(p) >= 3))\n .slice(0, 6);\n\nconst progression = untaughtWeeks.flatMap(w => \n w.concepts.map(c => ({\n name: c.toLowerCase().trim(),\n weight: 2,\n week_id: w.week_id,\n source: 'progression'\n }))\n);\n\n// 6. Review concepts (seen once, >2 cycles ago)\nconst reviews = db.prepare(\n 'SELECT concept_name FROM concept_mastery WHERE times_seen = 1 AND last_cycle_id < ?'\n).all(currentCycleId - 2).map(r => ({\n name: r.concept_name,\n weight: 1,\n source: 'review'\n}));\n\n// 7. Weighted random sampling\nconst pool = [...gaps, ...progression, ...reviews];\nconst totalWeight = pool.reduce((sum, item) => sum + item.weight, 0);\n\nfunction weightedSample(pool, count) {\n const selected = [];\n const poolCopy = [...pool];\n \n for (let i = 0; i < Math.min(count, poolCopy.length); i++) {\n const totalW = poolCopy.reduce((sum, item) => sum + item.weight, 0);\n let random = Math.random() * totalW;\n \n for (let j = 0; j < poolCopy.length; j++) {\n random -= poolCopy[j].weight;\n if (random <= 0) {\n selected.push(poolCopy[j]);\n poolCopy.splice(j, 1);\n break;\n }\n }\n }\n \n return selected;\n}\n\nconst selected = weightedSample(pool, 14);\nconst selectedNames = selected.map(s => s.name);\nconst priorityWeeks = [...new Set(selected.filter(s => s.week_id).map(s => s.week_id))];\n\ndb.close();\n\nreturn {\n json: {\n assessment,\n selected_concepts: selectedNames,\n priority_weeks: priorityWeeks,\n selection_breakdown: {\n gaps: gaps.length,\n progression: progression.length,\n reviews: reviews.length,\n selected: selected.length\n }\n }\n};"
},
"id": "select-concepts",
"name": "Select Priority Concepts",
"type": "n8n-nodes-base.code",
"position": [
2050,
400
],
"typeVersion": 2
},
{
"parameters": {
"model": "gpt-4-turbo-preview",
"messages": {
"values": [
{
"role": "system",
"content": "You are an expert Go developer and curriculum designer acting as a personal tutor.\n\nGenerate the next 4-week curriculum using these priority concepts:\n{{ $json.selected_concepts.join(', ') }}\n\nRecommended untaught weeks: {{ $json.priority_weeks.join(', ') }}\n\nSTRICT JSON OUTPUT FORMAT:\n{\n \"objective\": \"string (overall learning goal for this 4-week block)\",\n \"weeks\": [\n {\n \"week_number\": int (1-4),\n \"primary_topic\": \"string\",\n \"key_concepts\": [\"string array\"],\n \"capstone\": {\n \"title\": \"string\",\n \"description\": \"string (2-3 sentences)\",\n \"requirements\": [\"string array of 4-6 actionable bullets\"]\n }\n }\n ]\n}\n\nCRITICAL RULES:\n1. Each week must build on previous week\n2. If any selected concept has prerequisite that student scored <3, add to capstone requirements: \"REVIEW [concept] from previous cycle - you scored [X]/5, practice this again\"\n3. Maintain strong pedagogical flow, don't force-fit concepts awkwardly\n4. The goal is progressing toward \"Hirable Backend Engineer\"\n5. After Go Fundamentals, typical progression is: Web APIs \u2192 Databases \u2192 Microservices \u2192 DevOps"
},
{
"role": "user",
"content": "=# ASSESSMENT OF PREVIOUS CYCLE\n\n**Essay Strengths:**\n{{ $json.assessment.essay_strengths.join(', ') }}\n\n**Essay Gaps:**\n{{ $json.assessment.essay_gaps.join(', ') }}\n\n**Code Scores:**\n- Correctness: {{ $json.assessment.code_correctness }}/5\n- Idioms: {{ $json.assessment.code_idioms }}/5\n- Completeness: {{ $json.assessment.code_completeness }}/5\n\n**Concept Scores:**\n{{ $json.assessment.concept_scores.map(c => `- ${c.concept}: ${c.score}/5`).join('\\n') }}\n\nGenerate the next 4-week curriculum that addresses these gaps while progressing forward."
}
]
},
"options": {
"temperature": 0.7,
"maxTokens": 2000
},
"jsonOutput": true
},
"id": "generate-curriculum",
"name": "Generate Next Curriculum",
"type": "@n8n/n8n-nodes-langchain.openAi",
"position": [
2250,
400
],
"typeVersion": 1.3
},
{
"parameters": {
"jsCode": "// Validate curriculum response\nconst response = JSON.parse($json.message.content);\n\nconst isValid = (\n response.objective &&\n Array.isArray(response.weeks) &&\n response.weeks.length === 4 &&\n response.weeks.every(w => \n w.week_number &&\n w.primary_topic &&\n w.capstone &&\n w.capstone.title &&\n w.capstone.requirements\n )\n);\n\nif (!isValid) {\n throw new Error('Invalid curriculum format from LLM');\n}\n\nreturn {\n json: {\n ...$ input.first().json,\n curriculum: response\n }\n};"
},
"id": "validate-curriculum",
"name": "Validate Curriculum",
"type": "n8n-nodes-base.code",
"position": [
2450,
400
],
"typeVersion": 2
},
{
"parameters": {
"jsCode": "const Database = require('better-sqlite3');\nconst db = new Database('e:/repos/golearn/data/curriculum.db');\n\n// Update existing active cycles to completed\ndb.prepare('UPDATE cycles SET status=? WHERE status=?').run('completed', 'active');\n\n// Get next cycle number\nconst maxCycle = db.prepare('SELECT COALESCE(MAX(cycle_num), 0) as max_num FROM cycles').get();\nconst newCycleNum = maxCycle.max_num + 1;\n\n// Insert new cycle\nconst curriculumJson = JSON.stringify($json.curriculum);\nconst result = db.prepare('INSERT INTO cycles (cycle_num, curriculum_json, status) VALUES (?, ?, ?)').run(newCycleNum, curriculumJson, 'active');\n\nconst newCycleId = result.lastInsertRowid;\ndb.close();\n\nreturn {\n json: {\n new_cycle_id: newCycleId,\n new_cycle_num: newCycleNum\n }\n};",
"mode": "runOnceForAllItems"
},
"id": "insert-new-cycle",
"name": "Insert New Cycle",
"type": "n8n-nodes-base.code",
"position": [
2650,
400
],
"typeVersion": 2
},
{
"parameters": {
"jsCode": "// Update concept mastery scores\nconst Database = require('better-sqlite3');\nconst db = new Database('e:/repos/golearn/data/curriculum.db');\n\nconst assessment = $input.first().json.assessment;\nconst cycleId = $json.new_cycle_id;\n\nassessment.concept_scores.forEach(c => {\n const normalized = c.concept.toLowerCase().trim();\n \n db.prepare(\n 'UPDATE concept_mastery SET current_score = ?, last_cycle_id = ?, times_seen = times_seen + 1 WHERE concept_name = ?'\n ).run(c.score, cycleId, normalized);\n});\n\ndb.close();\n\nreturn {\n json: {\n ...$input.first().json,\n cycle_id: cycleId,\n cycle_num: $json.new_cycle_num\n }\n};"
},
"id": "update-mastery",
"name": "Update Concept Mastery",
"type": "n8n-nodes-base.code",
"position": [
2850,
400
],
"typeVersion": 2
},
{
"parameters": {
"jsCode": "// Build plateau alerts\nconst Database = require('better-sqlite3');\nconst db = new Database('e:/repos/golearn/data/curriculum.db');\n\nconst assessment = $input.first().json.assessment;\n\nconst plateaus = assessment.concept_scores\n .filter(c => {\n const normalized = c.concept.toLowerCase().trim();\n const row = db.prepare(\n 'SELECT times_seen, current_score FROM concept_mastery WHERE concept_name = ?'\n ).get(normalized);\n \n return row && row.times_seen >= 3 && c.score < 3;\n })\n .map(c => {\n const normalized = c.concept.toLowerCase().trim();\n const row = db.prepare(\n 'SELECT times_seen FROM concept_mastery WHERE concept_name = ?'\n ).get(normalized);\n \n return {\n concept: c.concept,\n cycles_struggling: row.times_seen,\n message: `Struggled with ${c.concept} for ${row.times_seen} cycles`\n };\n });\n\ndb.close();\n\nreturn {\n json: {\n success: true,\n curriculum: $input.first().json.curriculum,\n assessment: {\n code_correctness: assessment.code_correctness,\n code_idioms: assessment.code_idioms,\n code_completeness: assessment.code_completeness,\n detailed_feedback: assessment.detailed_feedback,\n essay_strengths: assessment.essay_strengths,\n essay_gaps: assessment.essay_gaps,\n concept_scores: assessment.concept_scores\n },\n plateau_alerts: plateaus,\n next_cycle_num: $input.first().json.cycle_num\n }\n};"
},
"id": "build-response",
"name": "Build Response",
"type": "n8n-nodes-base.code",
"position": [
3050,
400
],
"typeVersion": 2
},
{
"parameters": {
"respondWith": "json",
"responseBody": "={{ $json }}",
"options": {}
},
"id": "respond-success",
"name": "Respond Success",
"type": "n8n-nodes-base.respondToWebhook",
"position": [
3250,
400
],
"typeVersion": 1
},
{
"parameters": {
"jsCode": "// Return bootstrap curriculum\nconst fs = require('fs');\nconst bootstrap = JSON.parse(fs.readFileSync('e:/repos/golearn/scripts/bootstrap-curriculum.json', 'utf8'));\n\nreturn {\n json: {\n success: true,\n curriculum: bootstrap,\n assessment: null,\n plateau_alerts: [],\n next_cycle_num: 1,\n message: 'Starting with bootstrap curriculum - complete Week 1-4 and submit your first assessment'\n }\n};"
},
"id": "return-bootstrap",
"name": "Return Bootstrap",
"type": "n8n-nodes-base.code",
"position": [
1050,
200
],
"typeVersion": 2
},
{
"parameters": {
"respondWith": "json",
"responseBody": "={{ $json }}",
"options": {}
},
"id": "respond-bootstrap",
"name": "Respond Bootstrap",
"type": "n8n-nodes-base.respondToWebhook",
"position": [
1250,
200
],
"typeVersion": 1
},
{
"parameters": {
"jsCode": "return {\n json: {\n success: false,\n error_type: error.name || 'workflow_error',\n error_details: error.message,\n message: 'Assessment failed. Please check your submission and try again.'\n }\n};"
},
"id": "error-handler",
"name": "Error Handler",
"type": "n8n-nodes-base.code",
"position": [
2450,
600
],
"typeVersion": 2
},
{
"parameters": {
"respondWith": "json",
"responseBody": "={{ $json }}",
"options": {
"responseCode": 500
}
},
"id": "respond-error",
"name": "Respond Error",
"type": "n8n-nodes-base.respondToWebhook",
"position": [
2650,
600
],
"typeVersion": 1
}
],
"connections": {
"Webhook Submit": {
"main": [
[
{
"node": "Check Current Cycle",
"type": "main",
"index": 0
}
]
]
},
"Check Current Cycle": {
"main": [
[
{
"node": "Is First Cycle?",
"type": "main",
"index": 0
}
]
]
},
"Is First Cycle?": {
"main": [
[
{
"node": "Read Bootstrap",
"type": "main",
"index": 0
}
],
[
{
"node": "Get Current Curriculum",
"type": "main",
"index": 0
}
]
]
},
"Read Bootstrap": {
"main": [
[
{
"node": "Insert Bootstrap Cycle",
"type": "main",
"index": 0
}
]
]
},
"Insert Bootstrap Cycle": {
"main": [
[
{
"node": "Return Bootstrap",
"type": "main",
"index": 0
}
]
]
},
"Return Bootstrap": {
"main": [
[
{
"node": "Respond Bootstrap",
"type": "main",
"index": 0
}
]
]
},
"Get Current Curriculum": {
"main": [
[
{
"node": "Extract Current Concepts",
"type": "main",
"index": 0
}
]
]
},
"Extract Current Concepts": {
"main": [
[
{
"node": "Parse GitHub URL",
"type": "main",
"index": 0
}
]
]
},
"Parse GitHub URL": {
"main": [
[
{
"node": "Fetch Code Files",
"type": "main",
"index": 0
}
]
]
},
"Fetch Code Files": {
"main": [
[
{
"node": "Combine Code Files",
"type": "main",
"index": 0
}
]
]
},
"Combine Code Files": {
"main": [
[
{
"node": "Assess Essay and Code",
"type": "main",
"index": 0
}
]
]
},
"Assess Essay and Code": {
"main": [
[
{
"node": "Select Priority Concepts",
"type": "main",
"index": 0
}
]
]
},
"Select Priority Concepts": {
"main": [
[
{
"node": "Generate Next Curriculum",
"type": "main",
"index": 0
}
]
]
},
"Generate Next Curriculum": {
"main": [
[
{
"node": "Validate Curriculum",
"type": "main",
"index": 0
}
]
]
},
"Validate Curriculum": {
"main": [
[
{
"node": "Insert New Cycle",
"type": "main",
"index": 0
}
]
]
},
"Insert New Cycle": {
"main": [
[
{
"node": "Update Concept Mastery",
"type": "main",
"index": 0
}
]
]
},
"Update Concept Mastery": {
"main": [
[
{
"node": "Build Response",
"type": "main",
"index": 0
}
]
]
},
"Build Response": {
"main": [
[
{
"node": "Respond Success",
"type": "main",
"index": 0
}
]
]
}
},
"settings": {
"executionTimeout": 180,
"saveExecutionProgress": true,
"saveManualExecutions": true
}
}
For the full experience including quality scoring and batch install features for each workflow upgrade to Pro
How this works
Students and educators gain a streamlined way to submit assignments directly through a secure webhook, ensuring timely feedback and progress tracking without manual uploads or emails. This workflow automates the ingestion of assignment files, verifies the submission cycle using custom code, and leverages OpenAI to analyse content against the current curriculum for instant insights. The key step involves reading binary files from a predefined bootstrap source via readBinaryFile, which populates the necessary educational context before processing with an HTTP request to external APIs.
Use this workflow for automated assignment handling in online courses or tutoring platforms where submissions arrive via webhooks, particularly when integrating AI-driven evaluations. Avoid it for non-digital assignments or environments lacking webhook support, such as offline classrooms. Common variations include adapting the code nodes to fetch curricula from Google Drive instead of local files, or adding email notifications post-submission for larger cohorts.
About this workflow
Main: Submit Assignment. Uses readBinaryFile, httpRequest, openAi. Webhook trigger; 22 nodes.
Source: https://github.com/bananacryevil/golearn/blob/c19402ea75c7774b66e202e976e0972b7b4b7b9a/n8n-workflows/2-main-submit.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.
Contact-Us. Uses emailSend, httpRequest, nocoDb, openAi. Webhook trigger; 7 nodes.
PDF Q&A System with Pinecone RAG. Uses openAi, httpRequest. Webhook trigger; 7 nodes.
AI Blog Post Generator. Uses openAi, httpRequest. Webhook trigger; 6 nodes.
AI SEO Meta Tag Generator. Uses httpRequest, openAi. Webhook trigger; 6 nodes.
RoboNuggets - Faceless POV AI Machine (R24). Uses scheduleTrigger, googleSheets, chainLlm, lmChatOpenAi. Scheduled trigger; 31 nodes.