This workflow corresponds to n8n.io template #12731 — we link there as the canonical source.
This workflow follows the Agent → Gmail 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 →
{
"id": "BFhrn9MsTI1pOyJm",
"name": "Multi-Course Assignment Ingestion, Grading, and Feedback Automation System",
"tags": [],
"nodes": [
{
"id": "69428c52-b403-4e3c-a48a-82e6bdff568b",
"name": "Google Forms Webhook",
"type": "n8n-nodes-base.webhook",
"position": [
-5280,
400
],
"parameters": {
"path": "assignment-submission",
"options": {},
"httpMethod": "POST",
"responseMode": "lastNode"
},
"typeVersion": 2.1
},
{
"id": "f4b2dc61-eab3-4905-be8f-7137e68364d9",
"name": "LMS API Polling Schedule",
"type": "n8n-nodes-base.scheduleTrigger",
"position": [
-5280,
592
],
"parameters": {
"rule": {
"interval": [
{
"field": "minutes",
"minutesInterval": 15
}
]
}
},
"typeVersion": 1.3
},
{
"id": "7e00c5f8-35f8-4c61-a791-2644df6f6b6e",
"name": "Workflow Configuration",
"type": "n8n-nodes-base.set",
"position": [
-5056,
400
],
"parameters": {
"options": {},
"assignments": {
"assignments": [
{
"id": "id-1",
"name": "lmsApiUrl",
"type": "string",
"value": "<__PLACEHOLDER_VALUE__LMS API Base URL__>"
},
{
"id": "id-2",
"name": "lmsApiKey",
"type": "string",
"value": "<__PLACEHOLDER_VALUE__LMS API Key__>"
},
{
"id": "id-3",
"name": "plagiarismThreshold",
"type": "number",
"value": 0.75
},
{
"id": "id-4",
"name": "latePenaltyPercent",
"type": "number",
"value": 10
},
{
"id": "id-5",
"name": "driveFolderId",
"type": "string",
"value": "<__PLACEHOLDER_VALUE__Google Drive Folder ID__>"
},
{
"id": "id-6",
"name": "slackChannelInstructors",
"type": "string",
"value": "<__PLACEHOLDER_VALUE__Slack Channel for Instructors__>"
},
{
"id": "id-7",
"name": "slackChannelAlerts",
"type": "string",
"value": "<__PLACEHOLDER_VALUE__Slack Channel for Alerts__>"
},
{
"id": "id-8",
"name": "pdfConversionApiUrl",
"type": "string",
"value": "https://api.html2pdf.app/v1/generate"
},
{
"id": "id-9",
"name": "maxRetries",
"type": "number",
"value": 3
},
{
"id": "id-10",
"name": "rateLimitDelayMs",
"type": "number",
"value": 1000
}
]
},
"includeOtherFields": true
},
"typeVersion": 3.4
},
{
"id": "7f7009d3-7d1c-43ef-817f-2b60381d52e9",
"name": "Fetch LMS Assignments",
"type": "n8n-nodes-base.httpRequest",
"position": [
-5056,
592
],
"parameters": {
"url": "={{ $('Workflow Configuration').first().json.lmsApiUrl }}/assignments/submissions",
"options": {
"timeout": 30000
},
"sendQuery": true,
"sendHeaders": true,
"authentication": "genericCredentialType",
"genericAuthType": "httpHeaderAuth",
"queryParameters": {
"parameters": [
{
"name": "status",
"value": "pending"
},
{
"name": "limit",
"value": "50"
}
]
},
"headerParameters": {
"parameters": [
{
"name": "Authorization",
"value": "=Bearer {{ $('Workflow Configuration').first().json.lmsApiKey }}"
}
]
}
},
"typeVersion": 4.3
},
{
"id": "c0be44a6-de29-4550-b76a-134f1be9504a",
"name": "Merge Form and API Data",
"type": "n8n-nodes-base.merge",
"position": [
-4832,
496
],
"parameters": {},
"typeVersion": 3.2
},
{
"id": "9c8fa136-225c-4f3b-b089-8c2f21de1912",
"name": "Normalize Submission Data",
"type": "n8n-nodes-base.set",
"position": [
-4608,
496
],
"parameters": {
"options": {},
"assignments": {
"assignments": [
{
"id": "id-1",
"name": "studentId",
"type": "string",
"value": "={{ $json.student_id || $json.studentId }}"
},
{
"id": "id-2",
"name": "studentEmail",
"type": "string",
"value": "={{ $json.student_email || $json.email }}"
},
{
"id": "id-3",
"name": "studentName",
"type": "string",
"value": "={{ $json.student_name || $json.name }}"
},
{
"id": "id-4",
"name": "courseId",
"type": "string",
"value": "={{ $json.course_id || $json.courseId }}"
},
{
"id": "id-5",
"name": "assignmentId",
"type": "string",
"value": "={{ $json.assignment_id || $json.assignmentId }}"
},
{
"id": "id-6",
"name": "submissionText",
"type": "string",
"value": "={{ $json.submission_text || $json.text || $json.content }}"
},
{
"id": "id-7",
"name": "submissionFile",
"type": "string",
"value": "={{ $json.submission_file || $json.file }}"
},
{
"id": "id-8",
"name": "submittedAt",
"type": "string",
"value": "={{ $json.submitted_at || $json.timestamp || $now }}"
},
{
"id": "id-9",
"name": "deadline",
"type": "string",
"value": "={{ $json.deadline || $json.due_date }}"
},
{
"id": "id-10",
"name": "submissionId",
"type": "string",
"value": "={{ $json.id || $runIndex }}-{{ $now.toMillis() }}"
}
]
},
"includeOtherFields": true
},
"typeVersion": 3.4
},
{
"id": "5f419bad-1137-4b72-afe4-92a02df8376f",
"name": "Validate Submission",
"type": "n8n-nodes-base.code",
"position": [
-4384,
496
],
"parameters": {
"jsCode": "const items = $input.all();\nconst validatedItems = [];\n\nfor (const item of items) {\n const data = item.json;\n const errors = [];\n \n // FERPA-compliant validation - check for required fields\n if (!data.studentId) errors.push('Missing student ID');\n if (!data.studentEmail) errors.push('Missing student email');\n if (!data.courseId) errors.push('Missing course ID');\n if (!data.assignmentId) errors.push('Missing assignment ID');\n if (!data.submissionText && !data.submissionFile) errors.push('Missing submission content');\n \n // Validate email format\n const emailRegex = /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/;\n if (data.studentEmail && !emailRegex.test(data.studentEmail)) {\n errors.push('Invalid email format');\n }\n \n validatedItems.push({\n json: {\n ...data,\n isValid: errors.length === 0,\n validationErrors: errors,\n validatedAt: new Date().toISOString()\n }\n });\n}\n\nreturn validatedItems;"
},
"typeVersion": 2
},
{
"id": "cacda2ad-1582-42a3-8a8f-5fb79484cead",
"name": "Check Validation Status",
"type": "n8n-nodes-base.if",
"position": [
-4160,
496
],
"parameters": {
"options": {},
"conditions": {
"options": {
"leftValue": "",
"caseSensitive": false,
"typeValidation": "loose"
},
"combinator": "and",
"conditions": [
{
"id": "id-1",
"operator": {
"type": "boolean",
"operation": "equals"
},
"leftValue": "={{ $('Validate Submission').item.json.isValid }}",
"rightValue": true
}
]
}
},
"typeVersion": 2.3
},
{
"id": "b1505af3-2393-42b4-a06c-961320cb0f86",
"name": "Check Duplicate Submission",
"type": "n8n-nodes-base.postgres",
"position": [
-3936,
400
],
"parameters": {
"query": "SELECT COUNT(*) as count FROM submissions WHERE student_id = $1 AND assignment_id = $2 AND course_id = $3",
"options": {
"queryReplacement": "={{ $json.studentId }},={{ $json.assignmentId }},={{ $json.courseId }}"
},
"operation": "executeQuery"
},
"typeVersion": 2.6
},
{
"id": "5b304ffb-ea6e-41c0-8b5a-f9c216b5467f",
"name": "Is Duplicate?",
"type": "n8n-nodes-base.if",
"position": [
-3712,
400
],
"parameters": {
"options": {},
"conditions": {
"options": {
"leftValue": "",
"caseSensitive": false,
"typeValidation": "loose"
},
"combinator": "and",
"conditions": [
{
"id": "id-1",
"operator": {
"type": "number",
"operation": "gt"
},
"leftValue": "={{ $('Check Duplicate Submission').item.json.count }}",
"rightValue": "0"
}
]
}
},
"typeVersion": 2.3
},
{
"id": "de585b70-1cd4-46f3-afb9-3f5592afa9f1",
"name": "Calculate Deadline Penalty",
"type": "n8n-nodes-base.code",
"position": [
-3488,
400
],
"parameters": {
"jsCode": "const items = $input.all();\nconst config = $('Workflow Configuration').first().json;\nconst processedItems = [];\n\nfor (const item of items) {\n const data = item.json;\n const submittedAt = new Date(data.submittedAt);\n const deadline = new Date(data.deadline);\n \n let isLate = false;\n let daysLate = 0;\n let penaltyPercent = 0;\n let penaltyPoints = 0;\n \n if (submittedAt > deadline) {\n isLate = true;\n const diffMs = submittedAt - deadline;\n daysLate = Math.ceil(diffMs / (1000 * 60 * 60 * 24));\n penaltyPercent = Math.min(daysLate * config.latePenaltyPercent, 100);\n }\n \n processedItems.push({\n json: {\n ...data,\n isLate,\n daysLate,\n penaltyPercent,\n submissionStatus: isLate ? 'late' : 'on_time',\n processedAt: new Date().toISOString()\n }\n });\n}\n\nreturn processedItems;"
},
"typeVersion": 2
},
{
"id": "961cb641-7e6c-40df-866a-83a15b42d7b6",
"name": "Upload Submission to Drive",
"type": "n8n-nodes-base.googleDrive",
"position": [
-3040,
400
],
"parameters": {
"name": "={{ 'submission_' + $json.submissionId + '_' + $json.studentId + '.txt' }}",
"driveId": {
"__rl": true,
"mode": "list",
"value": "My Drive"
},
"options": {},
"folderId": {
"__rl": true,
"mode": "id",
"value": "={{ $('Workflow Configuration').first().json.driveFolderId }}"
}
},
"credentials": {
"googleDriveOAuth2Api": {
"name": "<your credential>"
}
},
"typeVersion": 3
},
{
"id": "bffdd766-b69b-45c9-9c33-8344f0b25a44",
"name": "Plagiarism Check Agent",
"type": "@n8n/n8n-nodes-langchain.agent",
"position": [
-2816,
400
],
"parameters": {
"text": "=Student Submission:\n{{ $json.submissionText }}\n\nCourse: {{ $json.courseId }}\nAssignment: {{ $json.assignmentId }}",
"options": {
"systemMessage": "You are a plagiarism detection specialist for academic institutions. Your task is to analyze student submissions and detect potential plagiarism.\n\nAnalyze the provided submission for:\n1. Common plagiarism patterns (copy-paste indicators, inconsistent writing style)\n2. Unusual vocabulary or phrasing for the academic level\n3. Lack of original thought or analysis\n4. Suspicious formatting or structure changes\n\nProvide a plagiarism score from 0.0 (no plagiarism) to 1.0 (definite plagiarism) and list specific concerns.\n\nIMPORTANT: This is for educational integrity purposes only. Handle all student data with FERPA compliance."
},
"promptType": "define",
"hasOutputParser": true
},
"typeVersion": 3.1
},
{
"id": "cdad1462-2865-4545-97a7-de26e949d44b",
"name": "OpenAI Model - Plagiarism",
"type": "@n8n/n8n-nodes-langchain.lmChatOpenAi",
"position": [
-2816,
624
],
"parameters": {
"model": {
"__rl": true,
"mode": "id",
"value": "gpt-4o"
},
"options": {
"maxTokens": 2000,
"temperature": 0.3
},
"builtInTools": {}
},
"credentials": {
"openAiApi": {
"name": "<your credential>"
}
},
"typeVersion": 1.3
},
{
"id": "f5123240-2fb7-4d67-9dcd-153702fdeb60",
"name": "Plagiarism Result Parser",
"type": "@n8n/n8n-nodes-langchain.outputParserStructured",
"position": [
-2688,
624
],
"parameters": {
"schemaType": "manual",
"inputSchema": "{\n \"type\": \"object\",\n \"properties\": {\n \"plagiarismScore\": {\n \"type\": \"number\",\n \"description\": \"Score from 0.0 to 1.0 indicating plagiarism likelihood\"\n },\n \"concerns\": {\n \"type\": \"array\",\n \"items\": { \"type\": \"string\" },\n \"description\": \"List of specific plagiarism concerns found\"\n },\n \"recommendation\": {\n \"type\": \"string\",\n \"description\": \"Recommendation: accept, flag_for_review, or reject\"\n }\n },\n \"required\": [\"plagiarismScore\", \"concerns\", \"recommendation\"]\n}"
},
"typeVersion": 1.3
},
{
"id": "77230e4c-f858-4913-a237-4f86ce77e605",
"name": "AI Grading Agent",
"type": "@n8n/n8n-nodes-langchain.agent",
"position": [
-2464,
400
],
"parameters": {
"text": "=Student Submission:\n{{ $json.submissionText }}\n\nCourse: {{ $json.courseId }}\nAssignment: {{ $json.assignmentId }}\nPlagiarism Score: {{ $json.plagiarismScore }}\nLate Status: {{ $json.submissionStatus }}",
"options": {
"systemMessage": "You are an expert academic grader for higher education. Your task is to grade student submissions fairly and provide constructive feedback.\n\nGrading Criteria:\n1. Content Quality (40%): Depth of analysis, critical thinking, originality\n2. Structure & Organization (20%): Logical flow, clear arguments, proper formatting\n3. Evidence & Support (20%): Use of sources, examples, data to support claims\n4. Writing Quality (20%): Grammar, clarity, academic tone\n\nProvide:\n- Overall score (0-100)\n- Breakdown by criteria\n- Constructive feedback highlighting strengths and areas for improvement\n- Specific suggestions for improvement\n\nIMPORTANT: Be fair, consistent, and encouraging. Handle all student data with FERPA compliance."
},
"promptType": "define",
"hasOutputParser": true
},
"typeVersion": 3.1
},
{
"id": "8fdb67d5-f729-49ae-8189-814cf3b54c83",
"name": "OpenAI Model - Grading",
"type": "@n8n/n8n-nodes-langchain.lmChatOpenAi",
"position": [
-2464,
624
],
"parameters": {
"model": {
"__rl": true,
"mode": "id",
"value": "gpt-4o"
},
"options": {
"maxTokens": 3000,
"temperature": 0.5
},
"builtInTools": {}
},
"credentials": {
"openAiApi": {
"name": "<your credential>"
}
},
"typeVersion": 1.3
},
{
"id": "1f37d1da-f907-48b9-b052-28c27ce9465a",
"name": "Grading Result Parser",
"type": "@n8n/n8n-nodes-langchain.outputParserStructured",
"position": [
-2336,
624
],
"parameters": {
"schemaType": "manual"
},
"typeVersion": 1.3
},
{
"id": "15cb8285-4513-43fb-b475-1598fe030611",
"name": "Combine Grading Results",
"type": "n8n-nodes-base.set",
"position": [
-2112,
400
],
"parameters": {
"options": {},
"assignments": {
"assignments": [
{
"id": "id-1",
"name": "finalScore",
"type": "number",
"value": "={{ Math.max(0, $json.overallScore * (1 - $json.penaltyPercent / 100)) }}"
},
{
"id": "id-2",
"name": "scoreBeforePenalty",
"type": "number",
"value": "={{ $json.overallScore }}"
},
{
"id": "id-3",
"name": "needsReview",
"type": "boolean",
"value": "={{ $json.plagiarismScore > $('Workflow Configuration').first().json.plagiarismThreshold || $json.overallScore < 50 }}"
},
{
"id": "id-4",
"name": "exceptionType",
"type": "string",
"value": "={{ $json.plagiarismScore > $('Workflow Configuration').first().json.plagiarismThreshold ? \"high_plagiarism\" : ($json.isLate ? \"late_submission\" : ($json.overallScore < 50 ? \"manual_review\" : \"normal\")) }}"
},
{
"id": "id-5",
"name": "gradedAt",
"type": "string",
"value": "={{ $now }}"
}
]
},
"includeOtherFields": true
},
"typeVersion": 3.4
},
{
"id": "c48e2492-ab8b-4a16-9e53-0119ca75ee10",
"name": "Route by Exception Type",
"type": "n8n-nodes-base.switch",
"position": [
-1936,
688
],
"parameters": {
"rules": {
"values": [
{
"outputKey": "High Plagiarism",
"conditions": {
"options": {
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"operator": {
"type": "string",
"operation": "equals"
},
"leftValue": "={{ $json.exceptionType }}",
"rightValue": "high_plagiarism"
}
]
},
"renameOutput": true
},
{
"outputKey": "Late Submission",
"conditions": {
"options": {
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"operator": {
"type": "string",
"operation": "equals"
},
"leftValue": "={{ $json.exceptionType }}",
"rightValue": "late_submission"
}
]
},
"renameOutput": true
},
{
"outputKey": "Manual Review",
"conditions": {
"options": {
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"operator": {
"type": "string",
"operation": "equals"
},
"leftValue": "={{ $json.exceptionType }}",
"rightValue": "manual_review"
}
]
},
"renameOutput": true
},
{
"outputKey": "Normal",
"conditions": {
"options": {
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"operator": {
"type": "string",
"operation": "equals"
},
"leftValue": "={{ $json.exceptionType }}",
"rightValue": "normal"
}
]
},
"renameOutput": true
}
]
},
"options": {}
},
"typeVersion": 3.4
},
{
"id": "2521b39b-9245-42eb-8c48-13860ab8dd87",
"name": "Store Submission Record",
"type": "n8n-nodes-base.postgres",
"position": [
-1648,
384
],
"parameters": {
"table": {
"__rl": true,
"mode": "name",
"value": "submissions"
},
"schema": {
"__rl": true,
"mode": "list",
"value": "public"
},
"columns": {
"value": {
"is_late": "={{ $json.isLate }}",
"deadline": "={{ $json.deadline }}",
"course_id": "={{ $json.courseId }}",
"days_late": "={{ $json.daysLate }}",
"graded_at": "={{ $json.gradedAt }}",
"student_id": "={{ $json.studentId }}",
"final_score": "={{ $json.finalScore }}",
"needs_review": "={{ $json.needsReview }}",
"student_name": "={{ $json.studentName }}",
"submitted_at": "={{ $json.submittedAt }}",
"assignment_id": "={{ $json.assignmentId }}",
"drive_file_id": "={{ $json.id }}",
"overall_score": "={{ $json.overallScore }}",
"student_email": "={{ $json.studentEmail }}",
"submission_id": "={{ $json.submissionId }}",
"exception_type": "={{ $json.exceptionType }}",
"penalty_percent": "={{ $json.penaltyPercent }}",
"submission_text": "={{ $json.submissionText }}",
"plagiarism_score": "={{ $json.plagiarismScore }}"
},
"schema": [
{
"id": "submission_id",
"required": false,
"displayName": "submission_id",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "student_id",
"required": false,
"displayName": "student_id",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "student_email",
"required": false,
"displayName": "student_email",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "student_name",
"required": false,
"displayName": "student_name",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "course_id",
"required": false,
"displayName": "course_id",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "assignment_id",
"required": false,
"displayName": "assignment_id",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "submission_text",
"required": false,
"displayName": "submission_text",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "submitted_at",
"required": false,
"displayName": "submitted_at",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "deadline",
"required": false,
"displayName": "deadline",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "is_late",
"required": false,
"displayName": "is_late",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "days_late",
"required": false,
"displayName": "days_late",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "penalty_percent",
"required": false,
"displayName": "penalty_percent",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "plagiarism_score",
"required": false,
"displayName": "plagiarism_score",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "overall_score",
"required": false,
"displayName": "overall_score",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "final_score",
"required": false,
"displayName": "final_score",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "graded_at",
"required": false,
"displayName": "graded_at",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "needs_review",
"required": false,
"displayName": "needs_review",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "exception_type",
"required": false,
"displayName": "exception_type",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "drive_file_id",
"required": false,
"displayName": "drive_file_id",
"defaultMatch": false,
"canBeUsedToMatch": true
}
],
"mappingMode": "defineBelow",
"matchingColumns": []
},
"options": {}
},
"typeVersion": 2.6
},
{
"id": "fac69902-2b03-4076-9f74-07d339d212e1",
"name": "Update Gradebook",
"type": "n8n-nodes-base.postgres",
"position": [
-1424,
384
],
"parameters": {
"query": "INSERT INTO gradebook (student_id, course_id, assignment_id, score, graded_at, graded_by) VALUES ($1, $2, $3, $4, $5, $6) ON CONFLICT (student_id, course_id, assignment_id) DO UPDATE SET score = EXCLUDED.score, graded_at = EXCLUDED.graded_at",
"options": {
"queryReplacement": "={{ $json.studentId }},={{ $json.courseId }},={{ $json.assignmentId }},={{ $json.finalScore }},={{ $json.gradedAt }},AI_SYSTEM"
},
"operation": "executeQuery"
},
"typeVersion": 2.6
},
{
"id": "67dfa515-f82f-4baa-8a93-046a31512274",
"name": "Generate Feedback HTML",
"type": "n8n-nodes-base.html",
"position": [
-1200,
384
],
"parameters": {
"html": "=<!DOCTYPE html>\n<html>\n<head>\n <meta charset=\"UTF-8\">\n <style>\n body { font-family: Arial, sans-serif; max-width: 800px; margin: 40px auto; padding: 20px; }\n .header { background: #2c3e50; color: white; padding: 20px; border-radius: 5px; }\n .score-box { background: #3498db; color: white; padding: 15px; margin: 20px 0; border-radius: 5px; text-align: center; }\n .section { margin: 20px 0; padding: 15px; border-left: 4px solid #3498db; background: #ecf0f1; }\n .criteria { display: flex; justify-content: space-between; margin: 10px 0; }\n .feedback { background: white; padding: 15px; margin: 10px 0; border-radius: 5px; }\n ul { list-style-type: none; padding-left: 0; }\n li:before { content: '\u2713 '; color: #27ae60; font-weight: bold; }\n </style>\n</head>\n<body>\n <div class=\"header\">\n <h1>Assignment Feedback</h1>\n <p>Student: {{ $json.studentName }}</p>\n <p>Course: {{ $json.courseId }} | Assignment: {{ $json.assignmentId }}</p>\n </div>\n \n <div class=\"score-box\">\n <h2>Final Score: {{ Math.round($json.finalScore) }}/100</h2>\n {{ $json.isLate ? '<p>Late Penalty Applied: ' + $json.penaltyPercent + '%</p>' : '' }}\n </div>\n \n <div class=\"section\">\n <h3>Score Breakdown</h3>\n <div class=\"criteria\"><span>Content Quality:</span><span>{{ $json.contentQuality }}/100</span></div>\n <div class=\"criteria\"><span>Structure & Organization:</span><span>{{ $json.structureOrganization }}/100</span></div>\n <div class=\"criteria\"><span>Evidence & Support:</span><span>{{ $json.evidenceSupport }}/100</span></div>\n <div class=\"criteria\"><span>Writing Quality:</span><span>{{ $json.writingQuality }}/100</span></div>\n </div>\n \n <div class=\"section\">\n <h3>Strengths</h3>\n <ul>\n {{ $json.strengths.map(s => '<li>' + s + '</li>').join('') }}\n </ul>\n </div>\n \n <div class=\"section\">\n <h3>Areas for Improvement</h3>\n <ul>\n {{ $json.areasForImprovement.map(a => '<li>' + a + '</li>').join('') }}\n </ul>\n </div>\n \n <div class=\"feedback\">\n <h3>Detailed Feedback</h3>\n <p>{{ $json.detailedFeedback }}</p>\n </div>\n \n <div class=\"section\">\n <h3>Suggestions for Next Time</h3>\n <ul>\n {{ $json.suggestions.map(s => '<li>' + s + '</li>').join('') }}\n </ul>\n </div>\n \n <p style=\"text-align: center; color: #7f8c8d; margin-top: 40px;\">Graded on {{ new Date($json.gradedAt).toLocaleDateString() }}</p>\n</body>\n</html>"
},
"typeVersion": 1.2
},
{
"id": "d0779dc4-fc2b-426c-a9f3-73311f337e30",
"name": "Convert HTML to PDF",
"type": "n8n-nodes-base.httpRequest",
"position": [
-976,
384
],
"parameters": {
"url": "={{ $('Workflow Configuration').first().json.pdfConversionApiUrl }}",
"method": "POST",
"options": {
"response": {
"response": {
"responseFormat": "file"
}
}
},
"jsonBody": "={ \"html\": $json.html, \"options\": { \"format\": \"A4\", \"margin\": { \"top\": \"20mm\", \"bottom\": \"20mm\" } } }",
"sendBody": true,
"specifyBody": "json"
},
"typeVersion": 4.3
},
{
"id": "5897a182-78aa-42e3-abce-28f99b52f0bf",
"name": "Upload Feedback PDF",
"type": "n8n-nodes-base.googleDrive",
"position": [
-752,
384
],
"parameters": {
"name": "=feedback_{{ $json.submissionId }}_{{ $json.studentId }}.pdf",
"driveId": {
"__rl": true,
"mode": "list",
"value": "My Drive"
},
"options": {
"simplifyOutput": true
},
"folderId": {
"__rl": true,
"mode": "id",
"value": "={{ $('Workflow Configuration').first().json.driveFolderId }}"
}
},
"credentials": {
"googleDriveOAuth2Api": {
"name": "<your credential>"
}
},
"typeVersion": 3
},
{
"id": "04fe6057-f0af-4acf-88d3-4e93528aeff1",
"name": "Notify Instructors - Slack",
"type": "n8n-nodes-base.slack",
"position": [
-496,
272
],
"parameters": {
"text": "=\ud83d\udcdd Assignment Graded\n\nStudent: {{ $json.studentName }}\nCourse: {{ $json.courseId }}\nAssignment: {{ $json.assignmentId }}\nFinal Score: {{ Math.round($json.finalScore) }}/100\n{{ $json.isLate ? \"\u23f0 Late submission (\" + $json.daysLate + \" days)\" : \"\" }}\n\nFeedback PDF: {{ $json.webViewLink }}",
"select": "channel",
"channelId": {
"__rl": true,
"mode": "id",
"value": "={{ $('Workflow Configuration').first().json.slackChannelInstructors }}"
},
"otherOptions": {},
"authentication": "oAuth2"
},
"credentials": {
"slackOAuth2Api": {
"name": "<your credential>"
}
},
"typeVersion": 2.4
},
{
"id": "e98b7aba-eb3e-414e-b829-0b42a0056342",
"name": "Notify Instructors - Email",
"type": "n8n-nodes-base.gmail",
"position": [
-496,
464
],
"parameters": {
"sendTo": "<__PLACEHOLDER_VALUE__Instructor Email Address__>",
"message": "=Dear Instructor,\n\nA new assignment has been graded:\n\nStudent: {{ $json.studentName }} ({{ $json.studentEmail }})\nCourse: {{ $json.courseId }}\nAssignment: {{ $json.assignmentId }}\nFinal Score: {{ Math.round($json.finalScore) }}/100\n{{ $json.isLate ? \"Late submission: \" + $json.daysLate + \" days late\" : \"Submitted on time\" }}\n\nFeedback PDF has been uploaded to Google Drive.\n\nBest regards,\nAutomated Grading System",
"options": {},
"subject": "=Assignment Graded: {{ $json.courseId }} - {{ $json.assignmentId }}"
},
"credentials": {
"gmailOAuth2": {
"name": "<your credential>"
}
},
"typeVersion": 2.2
},
{
"id": "dc3f6a8f-4e3b-49ed-8972-abbbb90c0af0",
"name": "Send Student Feedback Email",
"type": "n8n-nodes-base.gmail",
"position": [
-496,
656
],
"parameters": {
"sendTo": "={{ $json.studentEmail }}",
"message": "=Dear {{ $json.studentName }},\n\nYour assignment has been graded. Here are your results:\n\nCourse: {{ $json.courseId }}\nAssignment: {{ $json.assignmentId }}\nFinal Score: {{ Math.round($json.finalScore) }}/100\n{{ $json.isLate ? \"Note: A late penalty of \" + $json.penaltyPercent + \"% was applied.\" : \"\" }}\n\nYour detailed feedback is attached as a PDF. Please review the feedback carefully and use the suggestions to improve your future work.\n\nKey Strengths:\n{{ $json.strengths.slice(0, 3).map(s => \"- \" + s).join(\"\\n\") }}\n\nAreas to Focus On:\n{{ $json.areasForImprovement.slice(0, 3).map(a => \"- \" + a).join(\"\\n\") }}\n\nIf you have questions about your grade or feedback, please reach out to your instructor.\n\nBest regards,\nYour Course Team",
"options": {
"attachmentsUi": {
"attachmentsBinary": [
{}
]
}
},
"subject": "=Your Assignment Feedback: {{ $json.courseId }} - {{ $json.assignmentId }}",
"emailType": "text"
},
"credentials": {
"gmailOAuth2": {
"name": "<your credential>"
}
},
"typeVersion": 2.2
},
{
"id": "30e4fd96-d4e0-400c-b4b5-33769d658dc2",
"name": "Alert - High Plagiarism",
"type": "n8n-nodes-base.slack",
"position": [
-1648,
960
],
"parameters": {
"text": "=\ud83d\udea8 HIGH PLAGIARISM ALERT\n\nStudent: {{ $json.studentName }} ({{ $json.studentEmail }})\nCourse: {{ $json.courseId }}\nAssignment: {{ $json.assignmentId }}\nPlagiarism Score: {{ Math.round($json.plagiarismScore * 100) }}%\n\nConcerns:\n{{ $json.concerns.map(c => \"\u2022 \" + c).join(\"\\n\") }}\n\nRecommendation: {{ $json.recommendation }}\n\nAction Required: Manual review needed",
"select": "channel",
"channelId": {
"__rl": true,
"mode": "id",
"value": "={{ $('Workflow Configuration').first().json.slackChannelAlerts }}"
},
"otherOptions": {},
"authentication": "oAuth2"
},
"credentials": {
"slackOAuth2Api": {
"name": "<your credential>"
}
},
"typeVersion": 2.4
},
{
"id": "fd4602d1-05c4-4e14-b9c6-71e043c01fb8",
"name": "Alert - Late Submission",
"type": "n8n-nodes-base.slack",
"position": [
-1648,
576
],
"parameters": {
"text": "=\u23f0 LATE SUBMISSION\n\nStudent: {{ $json.studentName }} ({{ $json.studentEmail }})\nCourse: {{ $json.courseId }}\nAssignment: {{ $json.assignmentId }}\nDays Late: {{ $json.daysLate }}\nPenalty Applied: {{ $json.penaltyPercent }}%\nScore Before Penalty: {{ Math.round($json.scoreBeforePenalty) }}\nFinal Score: {{ Math.round($json.finalScore) }}",
"select": "channel",
"channelId": {
"__rl": true,
"mode": "id",
"value": "={{ $('Workflow Configuration').first().json.slackChannelAlerts }}"
},
"otherOptions": {},
"authentication": "oAuth2"
},
"credentials": {
"slackOAuth2Api": {
"name": "<your credential>"
}
},
"typeVersion": 2.4
},
{
"id": "b2505612-a537-4094-908a-4477bbcf6e2e",
"name": "Alert - Manual Review Needed",
"type": "n8n-nodes-base.slack",
"position": [
-1648,
768
],
"parameters": {
"text": "=\u26a0\ufe0f MANUAL REVIEW NEEDED\n\nStudent: {{ $json.studentName }} ({{ $json.studentEmail }})\nCourse: {{ $json.courseId }}\nAssignment: {{ $json.assignmentId }}\nScore: {{ Math.round($json.finalScore) }}\n\nReason: Low score or quality concerns\n\nPlease review this submission manually.",
"select": "channel",
"channelId": {
"__rl": true,
"mode": "id",
"value": "={{ $('Workflow Configuration').first().json.slackChannelAlerts }}"
},
"otherOptions": {},
"authentication": "oAuth2"
},
"credentials": {
"slackOAuth2Api": {
"name": "<your credential>"
}
},
"typeVersion": 2.4
},
{
"id": "d791d734-1bf7-48e3-aec0-8bcb53caa0da",
"name": "Log Analytics Event",
"type": "n8n-nodes-base.postgres",
"position": [
-496,
848
],
"parameters": {
"table": {
"__rl": true,
"mode": "name",
"value": "analytics_events"
},
"schema": {
"__rl": true,
"mode": "list",
"value": "public"
},
"columns": {
"value": {
"score": "={{ $json.finalScore }}",
"is_late": "={{ $json.isLate }}",
"course_id": "={{ $json.courseId }}",
"timestamp": "={{ $json.gradedAt }}",
"event_type": "submission_graded",
"student_id": "={{ $json.studentId }}",
"assignment_id": "={{ $json.assignmentId }}",
"exception_type": "={{ $json.exceptionType }}",
"plagiarism_score": "={{ $json.plagiarismScore }}"
},
"schema": [
{
"id": "event_type",
"type": "string",
"display": true,
"required": false,
"displayName": "event_type",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "student_id",
"type": "string",
"display": true,
"required": false,
"displayName": "student_id",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "course_id",
"type": "string",
"display": true,
"required": false,
"displayName": "course_id",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "assignment_id",
"type": "string",
"display": true,
"required": false,
"displayName": "assignment_id",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "score",
"type": "number",
"display": true,
"required": false,
"displayName": "score",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "plagiarism_score",
"type": "number",
"display": true,
"required": false,
"displayName": "plagiarism_score",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "is_late",
"type": "boolean",
"display": true,
"required": false,
"displayName": "is_late",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "exception_type",
"type": "string",
"display": true,
"required": false,
"displayName": "exception_type",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "timestamp",
"type": "string",
"display": true,
"required": false,
"displayName": "timestamp",
"defaultMatch": false,
"canBeUsedToMatch": true
}
],
"mappingMode": "defineBelow",
"matchingColumns": [
"event_type",
"student_id",
"course_id",
"assignment_id",
"score",
"plagiarism_score",
"is_late",
"exception_type",
"timestamp"
]
},
"options": {}
},
"typeVersion": 2.6
},
{
"id": "de688fb4-1a9f-40c3-9d7a-6f870af5ac89",
"name": "Rate Limit Delay",
"type": "n8n-nodes-base.wait",
"position": [
-3264,
400
],
"parameters": {
"amount": "={{ $('Workflow Configuration').first().json.rateLimitDelayMs }}"
},
"typeVersion": 1.1
},
{
"id": "74e4fdef-5122-41b9-aafd-2be6defb5999",
"name": "Handle Validation Error",
"type": "n8n-nodes-base.set",
"position": [
-3936,
592
],
"parameters": {
"options": {},
"assignments": {
"assignments": [
{
"id": "id-1",
"name": "status",
"type": "string",
"value": "validation_failed"
},
{
"id": "id-2",
"name": "message",
"type": "string",
"value": "=Submission validation failed: {{ $json.validationErrors.join(\", \") }}"
},
{
"id": "id-3",
"name": "timestamp",
"type": "string",
"value": "={{ $now }}"
}
]
}
},
"typeVersion": 3.4
},
{
"id": "ed670307-0430-40f2-b4c0-e2ab6f976157",
"name": "Respond to Form Submission",
"type": "n8n-nodes-base.respondToWebhook",
"position": [
-3712,
592
],
"parameters": {
"options": {
"responseCode": 400
},
"respondWith": "json",
"responseBody": "={{ { \"success\": false, \"message\": $json.message, \"errors\": $json.validationErrors } }}"
},
"typeVersion": 1.5
},
{
"id": "fea532c0-40a9-4b21-8bc7-58c108034d74",
"name": "Sticky Note",
"type": "n8n-nodes-base.stickyNote",
"position": [
-5296,
-240
],
"parameters": {
"width": 912,
"height": 256,
"content": "## How It Works\nThis workflow automates business intelligence reporting by aggregating data from multiple sources, processing it through AI models, and delivering formatted dashboards via email. Designed for business analysts, operations managers, and executive teams, it solves the challenge of manually compiling metrics from disparate systems into coherent reports. The system triggers on schedule or webhook, extracting data from Google Sheets, databases, and APIs. Raw data flows through transformation nodes that calculate KPIs, generate trend analyses, and create visualizations. AI models (OpenAI) provide natural language insights and anomaly detection. Results populate multiple dashboard templates\u2014executive summary, departmental metrics, and detailed analytics\u2014each tailored to specific stakeholder needs. Formatted reports are automatically distributed via Gmail with embedded charts and actionable recommendations. This eliminates hours of manual data gathering, reduces reporting errors, and ensures stakeholders receive timely, consistent insights."
},
"typeVersion": 1
},
{
"id": "c5cea325-f8e9-40de-a5ad-7e205c638944",
"name": "Sticky Note1",
"type": "n8n-nodes-base.stickyNote",
"position": [
-4320,
-256
],
"parameters": {
"width": 496,
"height": 288,
"content": "## Setup Steps\n1. Configure Google Sheets credentials and specify source spreadsheet IDs\n2. Set up database connections (PostgreSQL, MySQL) with read-only access\n3. Add OpenAI API key for GPT-4 analytics and narrative generation\n4. Set Gmail OAuth credentials for automated email delivery\n5. Define recipient lists for each dashboard type (executive, departmental, detailed)\n6. Customize dashboard templates with company branding and preferred KPIs"
},
"typeVersion": 1
},
{
"id": "a5df6536-8f45-4703-b1ed-27988bbc1a73",
"name": "Sticky Note2",
"type": "n8n-nodes-base.stickyNote",
"position": [
-3728,
-304
],
"parameters": {
"color": 6,
"width": 464,
"height": 336,
"content": "## Prerequisites\nActive Google Workspace account with Sheets and Gmail access. \n## Use Cases\nAutomated weekly executive dashboards with YoY comparisons.\n## Customization\nModify dashboard templates to match corporate branding standards.\n## Benefits\nReduces report preparation time by 80% through full automation."
},
"typeVersion": 1
},
{
"id": "a27fb347-b6ad-4fb9-b81d-253792991bc8",
"name": "Sticky Note3",
"type": "n8n-nodes-base.stickyNote",
"position": [
-2880,
192
],
"parameters": {
"color": 7,
"width": 1136,
"height": 848,
"content": "## AI Analysis \nRoutes transformed data to OpenAI models for trend analysis, forecasting, and narrative generation.\n**Why** - Provides intelligent insights beyond basic calculations, highlighting patterns humans might overlook."
},
"typeVersion": 1
},
{
"id": "50819315-9d2f-4993-a35c-06fedb5afbc9",
"name": "Sticky Note4",
"type": "n8n-nodes-base.stickyNote",
"position": [
-4656,
192
],
"parameters": {
"color": 7,
"width": 1744,
"height": 688,
"content": "## Data Transformation \nCleanses, normalizes, and calculates KPIs using JavaScript code and mathematical functions.\n**Why** - Ensures data consistency and generates standardized metrics for cross-functional comparison."
},
"typeVersion": 1
},
{
"id": "433382eb-2849-47da-a164-31e096476699",
"name": "Sticky Note5",
"type": "n8n-nodes-base.stickyNote",
"position": [
-5344,
192
],
"parameters": {
"color": 7,
"width": 672,
"height": 672,
"content": "## Data Collection \nFetches raw data from Google Sheets, SQL databases, and REST APIs using scheduled triggers.\n**Why** - Centralizes dispersed business metrics into a single processing pipeline."
},
"typeVersion": 1
},
{
"id": "df59db2c-23f0-48a8-b72c-9b4371f9f15f",
"name": "Sticky Note6",
"type": "n8n-nodes-base.stickyNote",
"position": [
-1696,
192
],
"parameters": {
"color": 7,
"width": 1504,
"height": 944,
"content": "## Dashboard Generation \nPopulates pre-designed templates with processed data and AI-generated insights.\n**Why** - Creates stakeholder-specific views without requiring individual report customization."
},
"typeVersion": 1
}
],
"active": false,
"settings": {
"availableInMCP": false,
"executionOrder": "v1"
},
"versionId": "64a12e64-85fd-4e09-9e73-d0033bb0c9d7",
"connections": {
"Is Duplicate?": {
"main": [
[],
[
{
"node": "Calculate Deadline Penalty",
"type": "main",
"index": 0
}
]
]
},
"AI Grading Agent": {
"main": [
[
{
"node": "Combine Grading Results",
"type": "main",
"index": 0
}
]
]
},
"Rate Limit Delay": {
"main": [
[
{
"node": "Upload Submission to Drive",
"type": "main",
"index": 0
}
]
]
},
"Update Gradebook": {
"main": [
[
{
"node": "Generate Feedback HTML",
"type": "main",
"index": 0
}
]
]
},
"Convert HTML to PDF": {
"main": [
[
{
"node": "Upload Feedback PDF",
"type": "main",
"index": 0
}
]
]
},
"Upload Feedback PDF": {
"main": [
[
{
"node": "Notify Instructors - Slack",
"type": "main",
"index": 0
},
{
"node": "Notify Instructors - Email",
"type": "main",
"index": 0
},
{
"node": "Send Student Feedback Email",
"type": "main",
"index": 0
},
{
"node": "Log Analytics Event",
"type": "main",
"index": 0
}
]
]
},
"Validate Submission": {
"main": [
[
{
"node": "Check Validation Status",
"type": "main",
"index": 0
}
]
]
},
"Google Forms Webhook": {
"main": [
[
{
"node": "Workflow Configuration",
"type": "main",
"index": 0
}
]
]
},
"Fetch LMS Assignments": {
"main": [
[
{
"node": "Merge Form and API Data",
"type": "main",
"index": 0
}
]
]
},
"Grading Result Parser": {
"ai_outputParser": [
[
{
"node": "AI Grading Agent",
"type": "ai_outputParser",
"index": 0
}
]
]
},
"Generate Feedback HTML": {
"main": [
[
{
"node": "Convert HTML to PDF",
"type": "main",
"index": 0
}
]
]
},
"OpenAI Model - Grading": {
"ai_languageModel": [
[
{
"node": "AI Grading Agent",
"type": "ai_languageModel",
"index": 0
}
]
]
},
"Plagiarism Check Agent": {
"main": [
[
{
"node": "AI Grading Agent",
"type": "main",
"index": 0
}
]
]
},
"Workflow Configuration": {
"main": [
[
{
"node": "Merge Form and API Data",
"type": "main",
"index": 1
}
]
]
},
"Check Validation Status": {
"main": [
[
{
"node": "Check Duplicate Submission",
"type": "main",
"index": 0
}
],
[
{
"node": "Handle Validation Error",
"type": "main",
"index": 0
}
]
]
},
"Combine Grading Results": {
"main": [
[
{
"node": "Route by Exception Type",
"type": "main",
"index": 0
}
]
]
},
"Handle Validation Error": {
"main": [
[
{
"node": "Respond to Form Submission",
"type": "main",
"index": 0
}
]
]
},
"Merge Form and API Data": {
"main": [
[
{
"node": "Normalize Submission Data",
"type": "main",
"index": 0
}
]
]
},
"Route by Exception Type": {
"main": [
[
{
"node": "Alert - High Plagiarism",
"type": "main",
"index": 0
}
],
[
{
"node": "Alert - Late Submission",
"type": "main",
"index": 0
}
],
[
{
"node": "Alert - Manual Review Needed",
"type": "main",
"index": 0
}
],
[
{
"node": "Store Submission Record",
"type": "main",
"index": 0
}
]
]
},
"Store Submission Record": {
"main": [
[
{
"node": "Update Gradebook",
"type": "main",
"index": 0
}
]
]
},
"LMS API Polling Schedule": {
"main": [
[
{
"node": "Fetch LMS Assignments",
"type": "main",
"index": 0
}
]
]
},
"Plagiarism Result Parser": {
"ai_outputParser": [
[
{
"node": "Plagiarism Check Agent",
"type": "ai_outputParser",
"index": 0
}
]
]
},
"Normalize Submission Data": {
"main": [
[
{
"node": "Validate Submission",
"type": "main",
"index": 0
}
]
]
},
"OpenAI Model - Plagiarism": {
"ai_languageModel": [
[
{
"node": "Plagiarism Check Agent",
"type": "ai_languageModel",
"index": 0
}
]
]
},
"Calculate Deadline Penalty": {
"main": [
[
{
"node": "Rate Limit Delay",
"type": "main",
"index": 0
}
]
]
},
"Check Duplicate Submission": {
"main": [
[
{
"node": "Is Duplicate?",
"type": "main",
"index": 0
}
]
]
},
"Upload Submission to Drive": {
"main": [
[
{
"node": "Plagiarism Check Agent",
"type": "main",
"index": 0
}
]
]
}
}
}
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.
gmailOAuth2googleDriveOAuth2ApiopenAiApislackOAuth2Api
For the full experience including quality scoring and batch install features for each workflow upgrade to Pro
About this workflow
This workflow automates business intelligence reporting by aggregating data from multiple sources, processing it through AI models, and delivering formatted dashboards via email. Designed for business analysts, operations managers, and executive teams, it solves the challenge of…
Source: https://n8n.io/workflows/12731/ — 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.
Camila IA. Uses postgres, crypto, redis, agent. Webhook trigger; 92 nodes.
This advanced n8n workflow automates the full lead enrichment, qualification, and personalized outreach process tailored specifically for the B2B real estate sector. Integrating top platforms like Api
WooriFisa 최종. Uses memoryMongoDbChat, agent, httpRequest, documentDefaultDataLoader. Scheduled trigger; 68 nodes.
This n8n template automatically classifies incoming emails (Sales, Support, Internal, Finance, Promotions) and routes them to a dedicated OpenAI LLM Agent for processing. Depending on the category, th
This simple philosophy changes the way we think about automated sales agents. Context changes everything. In this 4-part workflow, we start by creating a knowledge base that will act as context across