{"workflow":{"id":14068,"name":"Track student attendance from CSV, email parents, and build an HTML dashboard","views":59,"recentViews":1,"totalViews":59,"createdAt":"2026-03-15T18:02:24.548Z","description":"## 🚀 Overview\n\nAutomatically monitors daily student attendance from CSV files, identifies absent students, sends parent email alerts via SMTP, calculates risk scores, and generates an interactive HTML dashboard — all on a weekday schedule with no manual work needed.\n\n---\n\n## ⚙️ How it works\n\n1. **Schedule trigger** — runs Monday–Friday at 17:30\n2. **Data ingestion** — reads `student_attendance.csv` and filters to today's records only\n3. **Absence check** — splits students into Absent / Present branches\n4. **Contact merge** — matches absent students with parent data from `student_contacts.csv`\n5. **Alert logic** — calculates risk level, attendance %, consecutive streak, and trend (Improving / Stable / Worsening)\n6. **Parent email** — sends a colour-coded HTML alert via SMTP for each absent student\n7. **Dashboard builder** — generates `dashboard.html` with 5 tabs: Today, Weekly, Monthly, Full History, and At-Risk\n8. **Report update** — appends records to `attendance_report.csv` for historical tracking\n\n---\n\n## 🛠 Setup steps\n\n1. Upload `student_attendance.csv` to the n8n files folder\n2. Upload `student_contacts.csv` to the n8n files folder\n3. Create an empty `attendance_report.csv` in the n8n files folder\n4. Add `smtp_user` in **Settings → Variables**\n5. Configure SMTP credentials in the **Send Email** node\n6. Adjust the cron schedule if needed (default: 17:30 Mon–Fri)\n\n---\n\n## 🔧 Required CSV formats\n\n**student_attendance.csv**\n\n| StudentID | Date | Status | Class | Subject | Teacher |\n|-----------|------|--------|-------|---------|---------|\n| S101 | 12-01-2026 | Absent | Grade 6 | Math | Mr Singh |\n\n**student_contacts.csv**\n\n| StudentID | ParentName | Email | Phone |\n|-----------|------------|-------|-------|\n| S101 | Anita Kumar | parent@email.com | +919999999999 |\n\n# 📊 Dashboard Preview\n![Dashboard Preview 1](https://raw.githubusercontent.com/tejasv694/n8n-template-images/refs/heads/main/Screenshot%202026-03-22%20105318.png)\n\n![ Dashboard Preview 2](https://raw.githubusercontent.com/tejasv694/n8n-template-images/refs/heads/main/Screenshot%202026-03-22%20105333.png)","workflow":{"id":"XsVaYkX86o40Zztx","meta":{"instanceId":"a37c127c368affab56704b03c743021f2ee87c596599b97c8cf708b183214a2d","templateCredsSetupCompleted":true},"name":"Track student attendance from CSV, alert parents by email, and generate an HTML dashboard","tags":[],"nodes":[{"id":"c933a392-8c2b-48b3-8757-ba850ab7537d","name":"Sticky Note","type":"n8n-nodes-base.stickyNote","position":[-384,-288],"parameters":{"width":500,"height":620,"content":"## How it works\n\n1. Runs every weekday at 17:30 (Mon–Fri)\n2. Reads `student_attendance.csv` and filters to today's records only\n3. Splits students into Absent / Present branches\n4. Fetches parent contacts from `student_contacts.csv` and merges the data\n5. Calculates risk level, attendance %, consecutive streak, and trend\n6. Sends a colour-coded HTML email alert to parents of absent students via SMTP\n7. Builds and saves an interactive `dashboard.html` with 5 tabs: Today, Weekly, Monthly, Full History, At-Risk\n8. Appends today's records to `attendance_report.csv` for historical tracking\n\n## Setup steps\n\n1. Upload `student_attendance.csv` to the n8n files folder\n2. Upload `student_contacts.csv` to the n8n files folder\n3. Create an empty `attendance_report.csv` in the n8n files folder\n4. Add `smtp_user` in n8n → Settings → Variables\n5. Configure SMTP credentials in the **Send Email** node\n6. Adjust the cron schedule if needed (default: 17:30 Mon–Fri)"},"typeVersion":1},{"id":"88bd7eff-365a-4891-be99-43f6c9235d0b","name":"Group Trigger Ingestion","type":"n8n-nodes-base.stickyNote","position":[144,-208],"parameters":{"color":7,"width":860,"height":420,"content":"## Trigger & data ingestion\n\nSchedule trigger reads attendance CSV and filters to today's records only."},"typeVersion":1},{"id":"254b824c-29fa-422d-82bf-d8288543cbd6","name":"Group Routing Merge","type":"n8n-nodes-base.stickyNote","position":[1040,-208],"parameters":{"color":7,"width":798,"height":424,"content":"## Absence routing & contact merge\n\nSplits absent vs present students, fetches parent contacts, and merges records."},"typeVersion":1},{"id":"9e76a543-d8ac-4906-aea1-1b4023a783f4","name":"Group Alert Logic","type":"n8n-nodes-base.stickyNote","position":[1856,-208],"parameters":{"color":7,"width":244,"height":416,"content":"## Alert logic\n\nCalculates risk level, attendance %, streak, and trend per student."},"typeVersion":1},{"id":"9dd75633-cd8e-4f97-bb65-6b27c32c937f","name":"Group Notifications","type":"n8n-nodes-base.stickyNote","position":[2144,-304],"parameters":{"color":7,"width":494,"height":272,"content":"## Parent notifications\n\nGenerates and sends a colour-coded HTML email alert to parents via SMTP."},"typeVersion":1},{"id":"66e18686-7781-48d3-8485-2f0e87fc11a6","name":"Group No Absence","type":"n8n-nodes-base.stickyNote","position":[2144,336],"parameters":{"color":7,"width":276,"height":274,"content":"## No absences branch\n\nSkips email and dashboard when all students are present today."},"typeVersion":1},{"id":"0d8e0e33-ad19-4222-967e-54b92efa97fc","name":"No Absences Today","type":"n8n-nodes-base.code","position":[2224,480],"parameters":{"jsCode":"// False branch: all students present today\nconst now = new Date(new Date().toLocaleString('en-US', {timeZone:'Asia/Kolkata'}));\nconst dd = String(now.getDate()).padStart(2,'0');\nconst mm = String(now.getMonth()+1).padStart(2,'0');\nconst yyyy = now.getFullYear();\nconst today = `${dd}-${mm}-${yyyy}`;\nconst nowStr = now.toLocaleString('en-IN', {timeZone:'Asia/Kolkata'});\nconst headers = 'Date,StudentID,Name,Class,Subject,Teacher,Status,AbsencesLast30Days,ConsecutiveStreak,AttendancePct,Trend,AlertLevel,AlertReason,ParentName,EmailSent,WhatsAppSent';\nreturn [{json:{todayRows:[], headers, nowStr, today, allAbsent:[], noAbsencesToday:true}}];\n"},"typeVersion":2},{"id":"7b23422e-e7d0-4c55-8eac-429ddc56ad26","name":"Recurring Time Trigger","type":"n8n-nodes-base.scheduleTrigger","position":[192,32],"parameters":{"rule":{"interval":[{"field":"cronExpression","expression":"30 17 * * 1-5"}]}},"typeVersion":1.1},{"id":"68e30462-a058-4c61-874b-530275978305","name":"Student Attendance Data","type":"n8n-nodes-base.readWriteFile","position":[416,32],"parameters":{"options":{},"fileSelector":"/home/node/.n8n-files/File_name.csv"},"typeVersion":1},{"id":"6f5f6070-20b2-4632-8943-80c8fd93b25f","name":"Extract Attendance CSV","type":"n8n-nodes-base.spreadsheetFile","position":[640,32],"parameters":{"options":{"rawData":true,"headerRow":true}},"typeVersion":2},{"id":"cb8c1959-cfa0-4d31-8439-2241bb9beedb","name":"IF: Is Absent?","type":"n8n-nodes-base.if","position":[1072,32],"parameters":{"options":{},"conditions":{"options":{"version":1,"leftValue":"","caseSensitive":true,"typeValidation":"strict"},"combinator":"and","conditions":[{"id":"cond-absent","operator":{"type":"string","operation":"equals"},"leftValue":"={{ $json.Status }}","rightValue":"Absent"}]}},"typeVersion":2},{"id":"f83c1d55-22ad-48d1-b3cd-40fc2ed447e7","name":"Students Contacts","type":"n8n-nodes-base.readWriteFile","position":[1264,-96],"parameters":{"options":{},"fileSelector":"/home/node/.n8n-files/File_name.csv"},"executeOnce":true,"typeVersion":1},{"id":"e8d3f1a6-94c3-49ac-b127-20c964159826","name":"Extract Contacts CSV","type":"n8n-nodes-base.spreadsheetFile","position":[1488,-96],"parameters":{"options":{"headerRow":true}},"executeOnce":true,"typeVersion":2},{"id":"06f4be2e-c00e-4314-a136-3364515a3340","name":"Merge Absent + Contacts","type":"n8n-nodes-base.merge","position":[1696,32],"parameters":{"mode":"combine","options":{},"mergeByFields":{"values":[{"field1":"StudentID","field2":"StudentID"}]}},"typeVersion":2.1},{"id":"8bc30b58-a119-4bba-9683-24807367b66e","name":"Alert Logic Block","type":"n8n-nodes-base.code","position":[1920,32],"parameters":{"jsCode":"function parseDate(str) {\n  const p = String(str||'').trim().split('-');\n  if (p.length===3) return new Date(+p[2],+p[1]-1,+p[0]);\n  return new Date(0);\n}\n\n// Always use Asia/Kolkata timezone for consistent \"today\" regardless of server TZ\nconst nowIST = new Date(new Date().toLocaleString('en-US', {timeZone:'Asia/Kolkata'}));\nconst todayStr = String(nowIST.getDate()).padStart(2,'0')+'-'+String(nowIST.getMonth()+1).padStart(2,'0')+'-'+nowIST.getFullYear();\nconst todayDate = parseDate(todayStr);\nconst thirtyAgo = new Date(todayDate); thirtyAgo.setDate(thirtyAgo.getDate()-30);\n\n// Normalize a raw date string — handles DD-MM-YYYY, YYYY-MM-DD, D/M/YYYY etc.\nfunction normalizeDate(raw) {\n  const s = String(raw||'').trim();\n  // Already DD-MM-YYYY\n  if (/^\\d{2}-\\d{2}-\\d{4}$/.test(s)) return s;\n  // YYYY-MM-DD (ISO)\n  if (/^\\d{4}-\\d{2}-\\d{2}$/.test(s)) {\n    const [y,m,d] = s.split('-');\n    return `${d}-${m}-${y}`;\n  }\n  // D/M/YYYY or DD/MM/YYYY\n  if (/^\\d{1,2}\\/\\d{1,2}\\/\\d{4}$/.test(s)) {\n    const [d,m,y] = s.split('/');\n    return String(d).padStart(2,'0')+'-'+String(m).padStart(2,'0')+'-'+y;\n  }\n  // Excel serial number (number only) — convert to date\n  if (/^\\d{5}$/.test(s)) {\n    const serial = parseInt(s);\n    const excelEpoch = new Date(1899,11,30);\n    const date = new Date(excelEpoch.getTime() + serial * 86400000);\n    return String(date.getDate()).padStart(2,'0')+'-'+String(date.getMonth()+1).padStart(2,'0')+'-'+date.getFullYear();\n  }\n  return s;\n}\n\nconst byStudent = {};\nfor (const item of $input.all()) {\n  const sid = String(item.json.StudentID||'').trim();\n  if (!sid) continue;\n  // Normalize the date on every row as it comes in\n  const normalizedDate = normalizeDate(item.json.Date);\n  const enriched = {...item.json, Date: normalizedDate};\n  if (!byStudent[sid]) byStudent[sid]=[];\n  byStudent[sid].push(enriched);\n}\n\nconst output = [];\nfor (const [sid,rows] of Object.entries(byStudent)) {\n  const contact = rows[0];\n  const last30 = rows.filter(r=>{const d=parseDate(r.Date);return d>=thirtyAgo&&d<=todayDate;}).sort((a,b)=>parseDate(a.Date)-parseDate(b.Date));\n  const absent30=last30.filter(r=>String(r.Status).toLowerCase()==='absent').length;\n  const total30=last30.length;\n  const attendancePct=total30>0?Math.round(((total30-absent30)/total30)*100):100;\n  let streak=0;\n  for(let i=last30.length-1;i>=0;i--){if(String(last30[i].Status).toLowerCase()==='absent')streak++;else break;}\n  const l7=last30.slice(-7),p7=last30.slice(-14,-7);\n  const ar=a=>a.length?a.filter(r=>String(r.Status).toLowerCase()==='absent').length/a.length:0;\n  const trend=ar(l7)>ar(p7)+0.1?'WORSENING':ar(l7)<ar(p7)-0.1?'IMPROVING':'STABLE';\n  let alertLevel='LOW',alertReason=`${absent30} absence(s) in 30 days`;\n  if(absent30>=5||streak>=3){alertLevel='HIGH';alertReason=streak>=3?`${streak} consecutive absences`:`${absent30} absences in 30 days`;}\n  else if(absent30>=3){alertLevel='MEDIUM';alertReason=`${absent30} absences in 30 days`;}\n  const todayRow=rows.find(r=>String(r.Date).trim()===todayStr)||rows[rows.length-1];\n  output.push({json:{studentId:sid,name:contact.Name||'Student',parentName:contact.ParentName||'Parent',email:contact.Email||'',phone:contact.Phone||'',class:todayRow.Class||contact.Class||'',subject:todayRow.Subject||'',teacher:todayRow.Teacher||'',date:todayStr,absencesLast30Days:absent30,consecutiveStreak:streak,attendancePct,trend,alertLevel,alertReason}});\n}\nreturn output;\n"},"typeVersion":2},{"id":"bc63d2c7-c79c-4bd7-8484-d52a3e6d4a66","name":"Generate Absence Email","type":"n8n-nodes-base.code","position":[2192,-176],"parameters":{"jsCode":"const item = $input.item.json;\nconst cfg = { HIGH:{color:'#dc2626',bg:'#fef2f2',border:'#fecaca',label:'Critical Alert',icon:'🔴'}, MEDIUM:{color:'#d97706',bg:'#fffbeb',border:'#fde68a',label:'Warning',icon:'🟡'}, LOW:{color:'#16a34a',bg:'#f0fdf4',border:'#bbf7d0',label:'Low Risk',icon:'🟢'} }[item.alertLevel]||{color:'#16a34a',bg:'#f0fdf4',border:'#bbf7d0',label:'Low Risk',icon:'🟢'};\nconst trendIcon={WORSENING:'📈 Worsening',STABLE:'➡️ Stable',IMPROVING:'📉 Improving'};\nconst subject=`${cfg.icon} Absence: ${item.name} — ${item.date} [${item.alertLevel}]`;\nconst body=`<html><body style=\"font-family:Arial,sans-serif;max-width:600px;margin:auto\"><div style=\"background:#f9fafb;border:1px solid #e5e7eb;border-radius:10px;padding:24px\"><h2>Absence Notification</h2><p style=\"color:#6b7280;font-size:13px\">${item.date}</p><div style=\"background:${cfg.bg};border-left:4px solid ${cfg.color};padding:12px;border-radius:4px;margin:16px 0\"><strong style=\"color:${cfg.color}\">${cfg.label}: ${item.alertReason}</strong></div><table style=\"width:100%;font-size:14px;border-collapse:collapse\"><tr><td style=\"padding:8px;color:#6b7280\">Student</td><td><b>${item.name}</b></td></tr><tr style=\"background:#f9f9f9\"><td style=\"padding:8px;color:#6b7280\">Class</td><td>${item.class}</td></tr><tr><td style=\"padding:8px;color:#6b7280\">Subject/Teacher</td><td>${item.subject} / ${item.teacher}</td></tr><tr style=\"background:#f9f9f9\"><td style=\"padding:8px;color:#6b7280\">Absences 30d</td><td><b>${item.absencesLast30Days}</b></td></tr><tr><td style=\"padding:8px;color:#6b7280\">Streak</td><td>${item.consecutiveStreak} day(s)</td></tr><tr style=\"background:#f9f9f9\"><td style=\"padding:8px;color:#6b7280\">Attendance</td><td><b>${item.attendancePct}%</b></td></tr><tr><td style=\"padding:8px;color:#6b7280\">Trend</td><td>${trendIcon[item.trend]||item.trend}</td></tr></table><p style=\"margin-top:16px;font-size:13px;color:#4b5563\">Dear ${item.parentName}, please contact the school if you have questions.</p></div></body></html>`;\nreturn [{json:{...item,emailSubject:subject,emailBody:body}}];\n"},"typeVersion":2},{"id":"30e57f21-7296-4684-ba90-0ac805470287","name":"Build Dashboard and Report","type":"n8n-nodes-base.code","position":[2192,112],"parameters":{"jsCode":"const allAbsent=$input.all().map(i=>i.json).filter(i=>i.studentId); // filter out empty items\nconst now=new Date(new Date().toLocaleString('en-US', {timeZone:'Asia/Kolkata'}));\nconst dd=String(now.getDate()).padStart(2,'0'),mm=String(now.getMonth()+1).padStart(2,'0'),yyyy=now.getFullYear();\nconst today=`${dd}-${mm}-${yyyy}`;\nconst nowStr=now.toLocaleString('en-IN',{timeZone:'Asia/Kolkata'});\nconst headers='Date,StudentID,Name,Class,Subject,Teacher,Status,AbsencesLast30Days,ConsecutiveStreak,AttendancePct,Trend,AlertLevel,AlertReason,ParentName,EmailSent,WhatsAppSent';\n\n// If no absences today (false branch), todayRows will be empty — dashboard shows \"No absences\"\nconst todayRows=allAbsent.map(s=>[today,s.studentId,s.name,s.class,s.subject,s.teacher,'Absent',s.absencesLast30Days,s.consecutiveStreak,s.attendancePct,s.trend,s.alertLevel,`\"${s.alertReason}\"`,s.parentName,s.email?'YES':'NO',s.phone?'YES':'NO'].join(','));\nreturn [{json:{todayRows,headers,nowStr,today,allAbsent,noAbsencesToday:allAbsent.length===0}}];\n"},"typeVersion":2},{"id":"4f2aa786-1404-4dc3-8687-02efe7edcc59","name":"Send Email","type":"n8n-nodes-base.emailSend","position":[2416,-176],"webhookId":"b67c1675-ff69-49af-96ff-ff2d531baf74","parameters":{"options":{},"subject":"={{ $json.emailSubject }}","toEmail":"={{ $json.email }}","fromEmail":"={{ $vars.smtp_user }}"},"typeVersion":2.1},{"id":"5c53845e-9b24-464e-abb2-5eb14c97fb4c","name":"Existing Report History","type":"n8n-nodes-base.readWriteFile","position":[2544,112],"parameters":{"options":{},"fileSelector":"/home/node/.n8n-files/File_name.csv"},"typeVersion":1},{"id":"3b86af26-3c7e-446f-b57f-e44bf6f6a34e","name":"Extract History CSV","type":"n8n-nodes-base.spreadsheetFile","position":[2752,112],"parameters":{"options":{"headerRow":true}},"typeVersion":2},{"id":"c9b04d9b-6bbd-48d1-84a8-204f9bd50571","name":"Combine History + Build Dashboard","type":"n8n-nodes-base.code","position":[2976,112],"parameters":{"jsCode":"// Handle both branches: true (Build Dashboard ran) or false (No Absences Today ran)\nlet built;\ntry { built = $('Build Dashboard and Report').first().json; } catch(e) {\n  try { built = $('No Absences Today').first().json; } catch(e2) {\n    const now2 = new Date(new Date().toLocaleString('en-US',{timeZone:'Asia/Kolkata'}));\n    const dd2=String(now2.getDate()).padStart(2,'0'), mm2=String(now2.getMonth()+1).padStart(2,'0'), yyyy2=now2.getFullYear();\n    built={todayRows:[],headers:'Date,StudentID,Name,Class,Subject,Teacher,Status,AbsencesLast30Days,ConsecutiveStreak,AttendancePct,Trend,AlertLevel,AlertReason,ParentName,EmailSent,WhatsAppSent',nowStr:now2.toLocaleString('en-IN',{timeZone:'Asia/Kolkata'}),today:`${dd2}-${mm2}-${yyyy2}`,allAbsent:[],noAbsencesToday:true};\n  }\n}\nconst {todayRows,headers,nowStr,today}=built;\n\n// History comes as plain JSON rows from Extract History CSV3 (spreadsheetFile node)\nconst historyRows=$input.all().map(i=>i.json).filter(r=>r.Date&&r.StudentID&&r.AlertLevel);\nconst historical=historyRows.filter(r=>String(r.Date).trim()!==today);\n\n// Parse today CSV lines into objects\nfunction parseCSVLine(line){\n  const res=[];let cur='',inQ=false;\n  for(const c of line){if(c==='\"'){inQ=!inQ;}else if(c===','&&!inQ){res.push(cur);cur='';}else cur+=c;}\n  res.push(cur);return res.map(v=>v.trim().replace(/^\"|\"$/g,''));\n}\nconst hdrArr=parseCSVLine(headers);\n// If no absences today, show a clean \"All Present\" message in Today tab\nconst noAbsencesToday = built.noAbsencesToday === true;\nconst todayObjs=todayRows.map(line=>{const v=parseCSVLine(line);const o={};hdrArr.forEach((h,i)=>o[h]=v[i]||'');return o;});\n\nconst allData=[...historical,...todayObjs];\n\nfunction toLine(r){\n  const reason=(r.AlertReason||'').includes(',')?`\"${r.AlertReason}\"`:r.AlertReason||'';\n  return [r.Date,r.StudentID,r.Name,r.Class,r.Subject,r.Teacher,r.Status,r.AbsencesLast30Days,r.ConsecutiveStreak,r.AttendancePct,r.Trend,r.AlertLevel,reason,r.ParentName,r.EmailSent,r.WhatsAppSent].join(',');\n}\nconst fullCsv=[headers,...allData.map(toLine)].join('\\n');\n\nfunction parseDDMMYYYY(s){const p=String(s).split('-');return p.length===3?new Date(+p[2],+p[1]-1,+p[0]):new Date(0);}\nfunction fmtMonth(s){const d=parseDDMMYYYY(s);return d.toLocaleString('en-IN',{month:'short',year:'numeric'});}\nfunction getWeekKey(s){const d=parseDDMMYYYY(s);const day=d.getDay()||7;d.setDate(d.getDate()-day+1);return String(d.getDate()).padStart(2,'0')+'-'+String(d.getMonth()+1).padStart(2,'0')+'-'+d.getFullYear();}\n\nconst allDates=[...new Set(allData.map(r=>r.Date))].sort((a,b)=>parseDDMMYYYY(a)-parseDDMMYYYY(b));\nconst allMonths=[...new Set(allDates.map(fmtMonth))];\nconst allWeeks=[...new Set(allDates.map(getWeekKey))].sort((a,b)=>parseDDMMYYYY(a)-parseDDMMYYYY(b));\nconst classes=[...new Set(allData.map(r=>r.Class))].filter(Boolean).sort();\nconst subjects=[...new Set(allData.map(r=>r.Subject))].filter(Boolean).sort();\nconst students=[...new Set(allData.map(r=>r.StudentID))].filter(Boolean);\n\n// ── Precompute per-student total days present/absent across all history ──\nconst studentStats={};\nfor(const sid of students){\n  const rows=allData.filter(r=>r.StudentID===sid);\n  const absent=rows.filter(r=>r.Status==='Absent').length;\n  const name=rows[0].Name||sid;\n  const cls=rows[0].Class||'';\n  const lastAlert=rows.sort((a,b)=>parseDDMMYYYY(b.Date)-parseDDMMYYYY(a.Date))[0].AlertLevel||'LOW';\n  const lastTrend=rows[0].Trend||'STABLE';\n  const lastAbs30=parseInt(rows[0].AbsencesLast30Days)||0;\n  const lastStreak=parseInt(rows[0].ConsecutiveStreak)||0;\n  const lastPct=parseFloat(rows[0].AttendancePct)||100;\n  studentStats[sid]={name,cls,absent,total:rows.length,lastAlert,lastTrend,lastAbs30,lastStreak,lastPct};\n}\n\nconst dateOpts=[...allDates].reverse().map(d=>`<option value=\"${d}\">${d}</option>`).join('');\nconst classOpts=classes.map(c=>`<option>${c}</option>`).join('');\nconst monthOpts=allMonths.map(m=>`<option value=\"${m}\">${m}</option>`).join('');\nconst weekOpts=allWeeks.map(w=>`<option value=\"${w}\">Week of ${w}</option>`).join('');\n\nconst dashHtml=`<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n<meta charset=\"UTF-8\"><meta name=\"viewport\" content=\"width=device-width,initial-scale=1\">\n<title>Student Attendance Dashboard</title>\n<script src=\"https://cdnjs.cloudflare.com/ajax/libs/Chart.js/4.4.0/chart.umd.min.js\"><\\/script>\n<style>\n*{box-sizing:border-box;margin:0;padding:0}\nbody{font-family:system-ui,sans-serif;background:#f0f2f5;color:#1f2937}\n.header{background:#1e293b;color:#fff;padding:14px 24px;display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:8px;box-shadow:0 2px 8px rgba(0,0,0,.2)}\n.header h1{font-size:17px;font-weight:600;letter-spacing:.01em}\n.header .sub{font-size:11px;opacity:.5;margin-top:2px}\n.updated{font-size:11px;opacity:.45;text-align:right;line-height:1.6}\n.main{padding:16px 20px;max-width:1500px;margin:auto}\n.topbar{display:flex;gap:8px;margin-bottom:14px;flex-wrap:wrap;align-items:center;background:#fff;padding:10px 16px;border-radius:10px;box-shadow:0 1px 4px rgba(0,0,0,.06)}\n.topbar label{font-size:11px;color:#6b7280;font-weight:500}\n.topbar select{padding:5px 10px;border:1px solid #e5e7eb;border-radius:6px;background:#fff;color:#1f2937;font-size:12px;cursor:pointer}\n.tabs{display:flex;gap:4px;margin-bottom:14px;flex-wrap:wrap}\n.tab{padding:7px 18px;border-radius:8px;font-size:13px;cursor:pointer;border:1px solid #e5e7eb;background:#fff;color:#64748b;font-weight:500;transition:all .15s}\n.tab:hover{background:#f8fafc}\n.tab.active{background:#1e293b;color:#fff;border-color:transparent}\n.section{display:none}.section.active{display:block}\n.kpis{display:grid;grid-template-columns:repeat(5,1fr);gap:10px;margin-bottom:14px}\n@media(max-width:1000px){.kpis{grid-template-columns:repeat(3,1fr)}.g2,.g3{grid-template-columns:1fr}}\n.kpi{background:#fff;border-radius:10px;padding:14px 16px;box-shadow:0 1px 4px rgba(0,0,0,.06)}\n.kpi .val{font-size:28px;font-weight:600;line-height:1}\n.kpi .lbl{font-size:10px;color:#9ca3af;margin-top:5px;text-transform:uppercase;letter-spacing:.06em}\n.kpi .sub{font-size:11px;margin-top:3px}\n.g2{display:grid;grid-template-columns:1fr 1fr;gap:12px;margin-bottom:12px}\n.g3{display:grid;grid-template-columns:1fr 1fr 1fr;gap:12px;margin-bottom:12px}\n.g13{display:grid;grid-template-columns:1fr 3fr;gap:12px;margin-bottom:12px}\n.card{background:#fff;border-radius:10px;padding:16px;box-shadow:0 1px 4px rgba(0,0,0,.06)}\n.card h3{font-size:10px;font-weight:600;color:#94a3b8;margin-bottom:14px;text-transform:uppercase;letter-spacing:.07em}\n.tbl{width:100%;border-collapse:collapse;font-size:12px}\n.tbl th{text-align:left;padding:8px 10px;color:#94a3b8;font-weight:600;border-bottom:2px solid #f1f5f9;font-size:10px;text-transform:uppercase;letter-spacing:.05em;position:sticky;top:0;background:#fff;z-index:1}\n.tbl td{padding:8px 10px;border-bottom:1px solid #f8fafc;color:#374151;vertical-align:middle}\n.tbl tr:last-child td{border-bottom:none}\n.tbl tr:hover td{background:#f8fafc}\n.badge{display:inline-flex;align-items:center;padding:2px 8px;border-radius:20px;font-size:10px;font-weight:700;letter-spacing:.02em}\n.b-high{background:#fee2e2;color:#dc2626}.b-med{background:#fef3c7;color:#d97706}.b-low{background:#dcfce7;color:#16a34a}\n.b-w{background:#fee2e2;color:#dc2626}.b-s{background:#f1f5f9;color:#64748b}.b-i{background:#dcfce7;color:#16a34a}\n.scroll{overflow-x:auto;max-height:400px;overflow-y:auto}\n.empty{text-align:center;padding:40px;color:#cbd5e1;font-size:13px}\n.bar-row{display:flex;align-items:center;gap:8px;margin-bottom:7px}\n.bar-row .bn{font-size:12px;color:#374151;width:90px;text-align:right;flex-shrink:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}\n.bar-wrap{flex:1;background:#f1f5f9;border-radius:4px;height:13px;overflow:hidden}\n.bar-fill{height:100%;border-radius:4px;transition:width .4s}\n.bar-row .bv{font-size:11px;color:#94a3b8;width:28px;text-align:right;flex-shrink:0}\n.month-card{background:#fff;border-radius:10px;padding:14px;box-shadow:0 1px 4px rgba(0,0,0,.06);border-left:4px solid #6366f1}\n.month-card h4{font-size:13px;font-weight:600;color:#1e293b;margin-bottom:8px}\n.month-stat{display:flex;justify-content:space-between;font-size:12px;color:#64748b;padding:3px 0;border-bottom:1px solid #f8fafc}\n.month-stat:last-child{border:none}\n.month-stat span:last-child{font-weight:600;color:#1e293b}\n.risk-badge{display:inline-block;width:8px;height:8px;border-radius:50%;margin-right:5px}\n.week-label{font-size:10px;color:#94a3b8;margin-bottom:6px;font-weight:600}\n<\\/style>\n<\\/head>\n<body>\n<div class=\"header\">\n  <div><h1>📊 Student Attendance Dashboard<\\/h1><div class=\"sub\">School Attendance Tracking System<\\/div><\\/div>\n  <div class=\"updated\">Last updated: ${nowStr}<br>Auto-updates daily at 17:30 <\\/div>\n<\\/div>\n<div class=\"main\">\n  <div class=\"topbar\">\n    <label>Class<\\/label><select id=\"fClass\" onchange=\"applyFilters()\"><option value=\"all\">All Classes<\\/option>${classOpts}<\\/select>\n    <label>Alert<\\/label><select id=\"fAlert\" onchange=\"applyFilters()\"><option value=\"all\">All Levels<\\/option><option>HIGH<\\/option><option>MEDIUM<\\/option><option>LOW<\\/option><\\/select>\n    <label>Trend<\\/label><select id=\"fTrend\" onchange=\"applyFilters()\"><option value=\"all\">All Trends<\\/option><option>WORSENING<\\/option><option>STABLE<\\/option><option>IMPROVING<\\/option><\\/select>\n  <\\/div>\n  <div class=\"tabs\">\n    <div class=\"tab active\"  onclick=\"switchTab('today',this)\">📅 Today<\\/div>\n    <div class=\"tab\" onclick=\"switchTab('weekly',this)\">📆 Weekly<\\/div>\n    <div class=\"tab\" onclick=\"switchTab('monthly',this)\">🗓 Monthly<\\/div>\n    <div class=\"tab\" onclick=\"switchTab('history',this)\">📚 Full History<\\/div>\n    <div class=\"tab\" onclick=\"switchTab('risk',this)\">🚨 At-Risk<\\/div>\n  <\\/div>\n\n  <!-- TODAY TAB -->\n  <div id=\"today\" class=\"section active\">\n    <div class=\"kpis\" id=\"todayKpis\"><\\/div>\n    <div class=\"g2\">\n      <div class=\"card\"><h3>Absent by Class — Today<\\/h3><div id=\"todayClassBar\"><\\/div><\\/div>\n      <div class=\"card\"><h3>Absent by Subject — Today<\\/h3><div id=\"todaySubjectBar\"><\\/div><\\/div>\n    <\\/div>\n    <div class=\"card\"><h3>Today's Absence Records<\\/h3><div class=\"scroll\"><table class=\"tbl\" id=\"todayTbl\"><\\/table><\\/div><\\/div>\n  <\\/div>\n\n  <!-- WEEKLY TAB -->\n  <div id=\"weekly\" class=\"section\">\n    <div class=\"topbar\" style=\"margin-bottom:14px\">\n      <label>Week<\\/label><select id=\"fWeek\" onchange=\"renderWeekly()\"><option value=\"all\">All Weeks<\\/option>${weekOpts}<\\/select>\n    <\\/div>\n    <div class=\"kpis\" id=\"weeklyKpis\"><\\/div>\n    <div class=\"g2\">\n      <div class=\"card\"><h3>Daily Absences This Week<\\/h3><canvas id=\"weeklyDailyChart\" height=\"160\"><\\/canvas><\\/div>\n      <div class=\"card\"><h3>Absent by Subject This Week<\\/h3><div id=\"weeklySubjectBar\"><\\/div><\\/div>\n    <\\/div>\n    <div class=\"g2\">\n      <div class=\"card\"><h3>Top Absent Students This Week<\\/h3><div class=\"scroll\"><table class=\"tbl\" id=\"weeklyTopTbl\"><\\/table><\\/div><\\/div>\n      <div class=\"card\"><h3>Alert Level Split This Week<\\/h3><canvas id=\"weeklyAlertDonut\" height=\"200\"><\\/canvas><\\/div>\n    <\\/div>\n  <\\/div>\n\n  <!-- MONTHLY TAB -->\n  <div id=\"monthly\" class=\"section\">\n    <div class=\"topbar\" style=\"margin-bottom:14px\">\n      <label>Month<\\/label><select id=\"fMonth\" onchange=\"renderMonthly()\"><option value=\"all\">All Months<\\/option>${monthOpts}<\\/select>\n    <\\/div>\n    <div class=\"kpis\" id=\"monthlyKpis\"><\\/div>\n    <div class=\"g2\">\n      <div class=\"card\"><h3>Monthly Absence Trend<\\/h3><canvas id=\"monthlyTrendChart\" height=\"160\"><\\/canvas><\\/div>\n      <div class=\"card\"><h3>Subject-wise Absences This Month<\\/h3><div id=\"monthlySubjectBar\"><\\/div><\\/div>\n    <\\/div>\n    <div class=\"g2\">\n      <div class=\"card\"><h3>Top Absent Students This Month<\\/h3><div class=\"scroll\"><table class=\"tbl\" id=\"monthlyTopTbl\"><\\/table><\\/div><\\/div>\n      <div class=\"card\"><h3>Class Attendance % This Month<\\/h3><canvas id=\"monthlyClassChart\" height=\"200\"><\\/canvas><\\/div>\n    <\\/div>\n    <div class=\"card\" style=\"margin-bottom:12px\"><h3>Month-by-Month Summary<\\/h3><div id=\"monthSummaryGrid\" style=\"display:grid;grid-template-columns:repeat(auto-fill,minmax(200px,1fr));gap:10px;padding-top:4px\"><\\/div><\\/div>\n  <\\/div>\n\n  <!-- FULL HISTORY TAB -->\n  <div id=\"history\" class=\"section\">\n    <div class=\"topbar\" style=\"margin-bottom:14px\">\n      <label>Date<\\/label><select id=\"fDate\" onchange=\"renderHistory()\"><option value=\"all\">All Dates<\\/option>${dateOpts}<\\/select>\n    <\\/div>\n    <div class=\"kpis\" id=\"histKpis\"><\\/div>\n    <div class=\"g2\">\n      <div class=\"card\"><h3>Daily Absence Count Over Time<\\/h3><canvas id=\"histDailyChart\" height=\"160\"><\\/canvas><\\/div>\n      <div class=\"card\"><h3>Attendance Rate by Class Over Time<\\/h3><canvas id=\"histClassChart\" height=\"160\"><\\/canvas><\\/div>\n    <\\/div>\n    <div class=\"g2\">\n      <div class=\"card\"><h3>Subject-wise Total Absences<\\/h3><div id=\"histSubjectBar\"><\\/div><\\/div>\n      <div class=\"card\"><h3>Alert Level Over Time<\\/h3><canvas id=\"histAlertChart\" height=\"200\"><\\/canvas><\\/div>\n    <\\/div>\n    <div class=\"card\"><h3>All Records<\\/h3><div class=\"scroll\"><table class=\"tbl\" id=\"histTbl\"><\\/table><\\/div><\\/div>\n  <\\/div>\n\n  <!-- AT-RISK TAB -->\n  <div id=\"risk\" class=\"section\">\n    <div class=\"kpis\" id=\"riskKpis\"><\\/div>\n    <div class=\"g2\">\n      <div class=\"card\"><h3>🔴 High Risk Students<\\/h3><div class=\"scroll\"><table class=\"tbl\" id=\"highRiskTbl\"><\\/table><\\/div><\\/div>\n      <div class=\"card\"><h3>🟡 Medium Risk Students<\\/h3><div class=\"scroll\"><table class=\"tbl\" id=\"medRiskTbl\"><\\/table><\\/div><\\/div>\n    <\\/div>\n    <div class=\"g2\">\n      <div class=\"card\"><h3>📈 Worsening Trend Students<\\/h3><div class=\"scroll\"><table class=\"tbl\" id=\"worsenTbl\"><\\/table><\\/div><\\/div>\n      <div class=\"card\"><h3>Top 10 Most Absent (All Time)<\\/h3><div id=\"topAbsentBar\"><\\/div><\\/div>\n    <\\/div>\n    <div class=\"card\"><h3>Subject-wise At-Risk Distribution<\\/h3><canvas id=\"riskSubjectChart\" height=\"120\"><\\/canvas><\\/div>\n  <\\/div>\n<\\/div>\n\n<script>\nconst ALL_DATA=${JSON.stringify(allData)};\nconst ALL_NO_ABSENCES_TODAY=${JSON.stringify(noAbsencesToday)};\nconst ALL_DATES=${JSON.stringify(allDates)};\nconst ALL_MONTHS=${JSON.stringify(allMonths)};\nconst ALL_WEEKS=${JSON.stringify(allWeeks)};\nconst ALL_CLASSES=${JSON.stringify(classes)};\nconst ALL_SUBJECTS=${JSON.stringify(subjects)};\nconst STUDENT_STATS=${JSON.stringify(studentStats)};\nconst TODAY='${today}';\n\nlet charts={};\nfunction parseDDMMYYYY(s){const p=String(s).split('-');return p.length===3?new Date(+p[2],+p[1]-1,+p[0]):new Date(0);}\nfunction fmtMonth(s){return parseDDMMYYYY(s).toLocaleString('en-IN',{month:'short',year:'numeric'});}\nfunction getWeekKey(s){const d=parseDDMMYYYY(s);const day=d.getDay()||7;d.setDate(d.getDate()-day+1);return String(d.getDate()).padStart(2,'0')+'-'+String(d.getMonth()+1).padStart(2,'0')+'-'+d.getFullYear();}\nfunction dc(id){if(charts[id]){charts[id].destroy();delete charts[id];}}\nfunction pct(a,t){return t===0?0:Math.round(a/t*100);}\n\n// Global filters\nfunction getFilters(){\n  return {\n    cls:document.getElementById('fClass').value,\n    alert:document.getElementById('fAlert').value,\n    trend:document.getElementById('fTrend').value\n  };\n}\nfunction applyGlobalFilter(rows){\n  const f=getFilters();\n  return rows.filter(r=>(f.cls==='all'||r.Class===f.cls)&&(f.alert==='all'||r.AlertLevel===f.alert)&&(f.trend==='all'||r.Trend===f.trend));\n}\n\nfunction makeBar(id,items,cf,showPct){\n  const el=document.getElementById(id);if(!el)return;\n  if(!items.length){el.innerHTML='<div class=\"empty\">No data<\\/div>';return;}\n  const mx=Math.max(...items.map(i=>i.val),1);\n  el.innerHTML=items.slice(0,10).map(i=>'<div class=\"bar-row\"><span class=\"bn\" title=\"'+i.label+'\">'+i.label+'<\\/span><div class=\"bar-wrap\"><div class=\"bar-fill\" style=\"width:'+pct(i.val,mx)+'%;background:'+cf(i)+'\"><\\/div><\\/div><span class=\"bv\">'+(showPct?i.val+'%':i.val)+'<\\/span><\\/div>').join('');\n}\nfunction donut(id,labels,vals,colors){\n  dc(id);const ctx=document.getElementById(id);if(!ctx)return;\n  charts[id]=new Chart(ctx,{type:'doughnut',data:{labels,datasets:[{data:vals,backgroundColor:colors,borderWidth:0,hoverOffset:4}]},options:{responsive:true,plugins:{legend:{position:'bottom',labels:{font:{size:11},padding:8}}},cutout:'62%'}});\n}\nfunction lineChart(id,labels,datasets,opts){\n  dc(id);const ctx=document.getElementById(id);if(!ctx)return;\n  charts[id]=new Chart(ctx,{type:'line',data:{labels,datasets},options:{responsive:true,interaction:{mode:'index',intersect:false},scales:{x:{grid:{color:'#f1f5f9'},ticks:{font:{size:10},maxTicksLimit:12}},y:{beginAtZero:true,grid:{color:'#f1f5f9'},ticks:{font:{size:10}}},...(opts||{})},...(opts||{}),plugins:{legend:{labels:{font:{size:11}}}}}});\n}\nfunction barChart(id,labels,datasets){\n  dc(id);const ctx=document.getElementById(id);if(!ctx)return;\n  charts[id]=new Chart(ctx,{type:'bar',data:{labels,datasets},options:{responsive:true,scales:{x:{grid:{display:false},ticks:{font:{size:10}}},y:{beginAtZero:true,grid:{color:'#f1f5f9'},ticks:{font:{size:10}}}},plugins:{legend:{labels:{font:{size:11}}}}}});\n}\n\nfunction tblHTML(cols,rows){\n  if(!rows.length)return '<tr><td class=\"empty\" colspan=\"'+cols.length+'\">No data<\\/td><\\/tr>';\n  return '<thead><tr>'+cols.map(c=>'<th>'+c+'<\\/th>').join('')+'<\\/tr><\\/thead><tbody>'+rows.join('')+'<\\/tbody>';\n}\nfunction alertBadge(l){return '<span class=\"badge '+(l==='HIGH'?'b-high':l==='MEDIUM'?'b-med':'b-low')+'\">'+l+'<\\/span>';}\nfunction trendBadge(t){return '<span class=\"badge '+(t==='WORSENING'?'b-w':t==='IMPROVING'?'b-i':'b-s')+'\">'+t+'<\\/span>';}\nfunction pctColor(p){return +p>=85?'#16a34a':+p>=70?'#d97706':'#dc2626';}\n\n// ── TODAY ──\nfunction renderToday(){\n  if(ALL_NO_ABSENCES_TODAY){\n    document.getElementById('todayKpis').innerHTML='<div class=\"kpi\" style=\"grid-column:1/-1;text-align:center;background:linear-gradient(135deg,#f0fdf4,#dcfce7);border:1px solid #bbf7d0\"><div class=\"val\" style=\"color:#16a34a;font-size:48px\">✅</div><div class=\"lbl\" style=\"font-size:16px;color:#16a34a;margin-top:8px\">No Absences Today</div><div class=\"sub\" style=\"color:#16a34a\">All students were present on '+TODAY+'</div></div>';\n    document.getElementById('todayClassBar').innerHTML='';\n    document.getElementById('todaySubjectBar').innerHTML='';\n    const tbl=document.getElementById('todayTbl');if(tbl)tbl.innerHTML='<tr><td class=\"empty\" colspan=\"10\" style=\"padding:40px;color:#16a34a;font-size:14px\">✅ All students were present today — no absence records.</td></tr>';\n    return;\n  }\n  const raw=ALL_DATA.filter(r=>r.Date===TODAY);\n  const data=applyGlobalFilter(raw);\n  const tot=data.length,high=data.filter(r=>r.AlertLevel==='HIGH').length,med=data.filter(r=>r.AlertLevel==='MEDIUM').length;\n  const wors=data.filter(r=>r.Trend==='WORSENING').length,sent=data.filter(r=>r.EmailSent==='YES').length;\n  const avgP=tot>0?Math.round(data.reduce((s,r)=>s+parseFloat(r.AttendancePct||0),0)/tot):0;\n  document.getElementById('todayKpis').innerHTML=kpiHTML([\n    {val:tot,lbl:'Absent Today',sub:TODAY,subc:'#64748b'},\n    {val:high,lbl:'High Risk',sub:med+' medium',subc:'#d97706',vc:'#dc2626'},\n    {val:wors,lbl:'Worsening',sub:data.filter(r=>r.Trend==='IMPROVING').length+' improving',subc:'#16a34a',vc:'#d97706'},\n    {val:avgP+'%',lbl:'Avg Attendance',sub:avgP>=85?'On target':'Below 85%',subc:avgP>=85?'#16a34a':'#dc2626'},\n    {val:sent,lbl:'Notified',sub:data.filter(r=>r.WhatsAppSent==='YES').length+' WhatsApp',subc:'#64748b',vc:'#16a34a'}\n  ]);\n  makeBar('todayClassBar',ALL_CLASSES.map(c=>({label:c,val:data.filter(r=>r.Class===c).length})),i=>i.val>5?'#ef4444':i.val>2?'#f59e0b':'#22c55e');\n  makeBar('todaySubjectBar',ALL_SUBJECTS.map(s=>({label:s,val:data.filter(r=>r.Subject===s).length})).sort((a,b)=>b.val-a.val),()=>'#6366f1');\n  const tbl=document.getElementById('todayTbl');\n  if(tbl)tbl.innerHTML=tblHTML(['Student','Name','Class','Subject','Teacher','Abs 30d','Streak','Rate','Trend','Alert'],\n    data.map(r=>'<tr><td>'+r.StudentID+'<\\/td><td>'+(r.Name||'—')+'<\\/td><td>'+r.Class+'<\\/td><td>'+(r.Subject||'—')+'<\\/td><td>'+(r.Teacher||'—')+'<\\/td><td>'+r.AbsencesLast30Days+'<\\/td><td>'+(+r.ConsecutiveStreak>0?'<b style=\"color:#dc2626\">'+r.ConsecutiveStreak+'d<\\/b>':'—')+'<\\/td><td><span style=\"color:'+pctColor(r.AttendancePct)+'\">'+parseFloat(r.AttendancePct).toFixed(1)+'%<\\/span><\\/td><td>'+trendBadge(r.Trend)+'<\\/td><td>'+alertBadge(r.AlertLevel)+'<\\/td><\\/tr>'));\n}\n\n// ── WEEKLY ──\nfunction renderWeekly(){\n  const selWeek=document.getElementById('fWeek')&&document.getElementById('fWeek').value;\n  const weekDates=selWeek&&selWeek!=='all'?ALL_DATES.filter(d=>getWeekKey(d)===selWeek):ALL_DATES.slice(-7);\n  const raw=applyGlobalFilter(ALL_DATA.filter(r=>weekDates.includes(r.Date)));\n  const tot=raw.length,high=raw.filter(r=>r.AlertLevel==='HIGH').length,med=raw.filter(r=>r.AlertLevel==='MEDIUM').length;\n  const wors=raw.filter(r=>r.Trend==='WORSENING').length;\n  const days=[...new Set(raw.map(r=>r.Date))].sort((a,b)=>parseDDMMYYYY(a)-parseDDMMYYYY(b));\n  document.getElementById('weeklyKpis').innerHTML=kpiHTML([\n    {val:tot,lbl:'Total Absences',sub:days.length+' school day(s)',subc:'#64748b'},\n    {val:high,lbl:'High Risk',sub:med+' medium',subc:'#d97706',vc:'#dc2626'},\n    {val:wors,lbl:'Worsening',sub:'trend',subc:'#64748b',vc:'#d97706'},\n    {val:tot>0?Math.round(tot/Math.max(days.length,1)):0,lbl:'Avg/Day',sub:'absences',subc:'#64748b'},\n    {val:[...new Set(raw.map(r=>r.StudentID))].length,lbl:'Unique Students',sub:'absent this week',subc:'#64748b'}\n  ]);\n  lineChart('weeklyDailyChart',days.map(d=>d.slice(0,5)),[\n    {label:'Absences',data:days.map(d=>raw.filter(r=>r.Date===d).length),borderColor:'#6366f1',backgroundColor:'rgba(99,102,241,0.1)',tension:.3,fill:true,pointRadius:4}\n  ]);\n  makeBar('weeklySubjectBar',ALL_SUBJECTS.map(s=>({label:s,val:raw.filter(r=>r.Subject===s).length})).sort((a,b)=>b.val-a.val),()=>'#6366f1');\n  // Top students this week by absence count\n  const sidCounts={};raw.forEach(r=>{sidCounts[r.StudentID]=(sidCounts[r.StudentID]||0)+1;});\n  const topRows=Object.entries(sidCounts).sort((a,b)=>b[1]-a[1]).slice(0,10);\n  const wTbl=document.getElementById('weeklyTopTbl');\n  if(wTbl)wTbl.innerHTML=tblHTML(['Student','Name','Class','Absences','Alert'],\n    topRows.map(([sid,cnt])=>{const st=STUDENT_STATS[sid]||{};return '<tr><td>'+sid+'<\\/td><td>'+(st.name||'—')+'<\\/td><td>'+(st.cls||'—')+'<\\/td><td><b>'+cnt+'<\\/b><\\/td><td>'+alertBadge(st.lastAlert||'LOW')+'<\\/td><\\/tr>';}));\n  donut('weeklyAlertDonut',['High','Medium','Low'],[high,med,raw.filter(r=>r.AlertLevel==='LOW').length],['#ef4444','#f59e0b','#22c55e']);\n}\n\n// ── MONTHLY ──\nfunction renderMonthly(){\n  const selMonth=document.getElementById('fMonth')&&document.getElementById('fMonth').value;\n  const curMonth=selMonth&&selMonth!=='all'?selMonth:(ALL_MONTHS[ALL_MONTHS.length-1]||'');\n  const monthData=applyGlobalFilter(ALL_DATA.filter(r=>fmtMonth(r.Date)===curMonth));\n  const tot=monthData.length,high=monthData.filter(r=>r.AlertLevel==='HIGH').length,med=monthData.filter(r=>r.AlertLevel==='MEDIUM').length;\n  const wors=monthData.filter(r=>r.Trend==='WORSENING').length;\n  const mDays=[...new Set(monthData.map(r=>r.Date))].sort((a,b)=>parseDDMMYYYY(a)-parseDDMMYYYY(b));\n  const avgP=tot>0?Math.round(monthData.reduce((s,r)=>s+parseFloat(r.AttendancePct||0),0)/tot):0;\n  document.getElementById('monthlyKpis').innerHTML=kpiHTML([\n    {val:tot,lbl:'Total Absences',sub:curMonth,subc:'#64748b'},\n    {val:high,lbl:'High Risk',sub:med+' medium',subc:'#d97706',vc:'#dc2626'},\n    {val:wors,lbl:'Worsening',sub:'trend this month',subc:'#64748b',vc:'#d97706'},\n    {val:avgP+'%',lbl:'Avg Attendance',sub:avgP>=85?'On target':'Below 85%',subc:avgP>=85?'#16a34a':'#dc2626'},\n    {val:mDays.length,lbl:'School Days',sub:'recorded this month',subc:'#64748b'}\n  ]);\n  // Monthly trend over all months\n  lineChart('monthlyTrendChart',ALL_MONTHS,[\n    {label:'Total Absences',data:ALL_MONTHS.map(m=>ALL_DATA.filter(r=>fmtMonth(r.Date)===m).length),borderColor:'#6366f1',backgroundColor:'rgba(99,102,241,0.08)',tension:.3,fill:true,pointRadius:4,pointBackgroundColor:'#6366f1'},\n    {label:'High Risk',data:ALL_MONTHS.map(m=>ALL_DATA.filter(r=>fmtMonth(r.Date)===m&&r.AlertLevel==='HIGH').length),borderColor:'#ef4444',backgroundColor:'transparent',tension:.3,pointRadius:3}\n  ]);\n  makeBar('monthlySubjectBar',ALL_SUBJECTS.map(s=>({label:s,val:monthData.filter(r=>r.Subject===s).length})).sort((a,b)=>b.val-a.val),()=>'#6366f1');\n  // Top students this month\n  const mSid={};monthData.forEach(r=>{mSid[r.StudentID]=(mSid[r.StudentID]||0)+1;});\n  const mTop=Object.entries(mSid).sort((a,b)=>b[1]-a[1]).slice(0,10);\n  const mTbl=document.getElementById('monthlyTopTbl');\n  if(mTbl)mTbl.innerHTML=tblHTML(['Student','Name','Class','Absences','Alert'],\n    mTop.map(([sid,cnt])=>{const st=STUDENT_STATS[sid]||{};return '<tr><td>'+sid+'<\\/td><td>'+(st.name||'—')+'<\\/td><td>'+(st.cls||'—')+'<\\/td><td><b>'+cnt+'<\\/b><\\/td><td>'+alertBadge(st.lastAlert||'LOW')+'<\\/td><\\/tr>';}));\n  // Class attendance % bar chart this month\n  barChart('monthlyClassChart',ALL_CLASSES,[\n    {label:'Avg Attendance %',data:ALL_CLASSES.map(c=>{const rows=monthData.filter(r=>r.Class===c);return rows.length?Math.round(rows.reduce((s,r)=>s+parseFloat(r.AttendancePct||0),0)/rows.length):0;}),backgroundColor:['#6366f1','#8b5cf6','#06b6d4','#10b981','#f59e0b'],borderRadius:6}\n  ]);\n  // Month summary cards\n  const grid=document.getElementById('monthSummaryGrid');\n  if(grid)grid.innerHTML=ALL_MONTHS.map(m=>{\n    const rows=ALL_DATA.filter(r=>fmtMonth(r.Date)===m);\n    const abs=rows.length,h=rows.filter(r=>r.AlertLevel==='HIGH').length,w=rows.filter(r=>r.Trend==='WORSENING').length;\n    const ap=rows.length?Math.round(rows.reduce((s,r)=>s+parseFloat(r.AttendancePct||0),0)/rows.length):0;\n    return '<div class=\"month-card\"><h4>'+m+'<\\/h4><div class=\"month-stat\"><span>Total Absences<\\/span><span>'+abs+'<\\/span><\\/div><div class=\"month-stat\"><span>High Risk<\\/span><span style=\"color:#dc2626\">'+h+'<\\/span><\\/div><div class=\"month-stat\"><span>Worsening<\\/span><span style=\"color:#d97706\">'+w+'<\\/span><\\/div><div class=\"month-stat\"><span>Avg Attendance<\\/span><span style=\"color:'+pctColor(ap)+'\">'+ap+'%<\\/span><\\/div><\\/div>';\n  }).join('');\n}\n\n// ── FULL HISTORY ──\nfunction renderHistory(){\n  const fDate=document.getElementById('fDate')&&document.getElementById('fDate').value;\n  const raw=fDate&&fDate!=='all'?ALL_DATA.filter(r=>r.Date===fDate):ALL_DATA;\n  const data=applyGlobalFilter(raw);\n  const tot=data.length,high=data.filter(r=>r.AlertLevel==='HIGH').length;\n  const wors=data.filter(r=>r.Trend==='WORSENING').length;\n  const avgP=tot>0?Math.round(data.reduce((s,r)=>s+parseFloat(r.AttendancePct||0),0)/tot):0;\n  document.getElementById('histKpis').innerHTML=kpiHTML([\n    {val:tot,lbl:'Total Records',sub:[...new Set(data.map(r=>r.Date))].length+' day(s)',subc:'#64748b'},\n    {val:high,lbl:'High Risk',sub:data.filter(r=>r.AlertLevel==='MEDIUM').length+' medium',subc:'#d97706',vc:'#dc2626'},\n    {val:wors,lbl:'Worsening',sub:data.filter(r=>r.Trend==='IMPROVING').length+' improving',subc:'#16a34a',vc:'#d97706'},\n    {val:avgP+'%',lbl:'Avg Attendance',sub:avgP>=85?'On target':'Below 85%',subc:avgP>=85?'#16a34a':'#dc2626'},\n    {val:ALL_MONTHS.length,lbl:'Months Tracked',sub:ALL_DATES.length+' total days',subc:'#64748b'}\n  ]);\n  lineChart('histDailyChart',ALL_DATES.map(d=>d.slice(0,5)),[\n    {label:'High',data:ALL_DATES.map(d=>ALL_DATA.filter(r=>r.Date===d&&r.AlertLevel==='HIGH').length),borderColor:'#ef4444',backgroundColor:'rgba(239,68,68,0.08)',tension:.3,fill:true,pointRadius:2},\n    {label:'Medium',data:ALL_DATES.map(d=>ALL_DATA.filter(r=>r.Date===d&&r.AlertLevel==='MEDIUM').length),borderColor:'#f59e0b',backgroundColor:'rgba(245,158,11,0.08)',tension:.3,fill:true,pointRadius:2},\n    {label:'Low',data:ALL_DATES.map(d=>ALL_DATA.filter(r=>r.Date===d&&r.AlertLevel==='LOW').length),borderColor:'#22c55e',backgroundColor:'rgba(34,197,94,0.06)',tension:.3,fill:true,pointRadius:2}\n  ]);\n  lineChart('histClassChart',ALL_DATES.map(d=>d.slice(0,5)),ALL_CLASSES.map((c,i)=>({\n    label:c,\n    data:ALL_DATES.map(d=>{const rows=ALL_DATA.filter(r=>r.Date===d&&r.Class===c);return rows.length?Math.round(rows.reduce((s,r)=>s+parseFloat(r.AttendancePct||0),0)/rows.length):null;}),\n    borderColor:['#6366f1','#8b5cf6','#06b6d4','#10b981','#f59e0b'][i%5],\n    backgroundColor:'transparent',tension:.3,pointRadius:1,spanGaps:true\n  })));\n  makeBar('histSubjectBar',ALL_SUBJECTS.map(s=>({label:s,val:data.filter(r=>r.Subject===s).length})).sort((a,b)=>b.val-a.val),()=>'#6366f1');\n  barChart('histAlertChart',ALL_MONTHS,[\n    {label:'High',data:ALL_MONTHS.map(m=>ALL_DATA.filter(r=>fmtMonth(r.Date)===m&&r.AlertLevel==='HIGH').length),backgroundColor:'#ef4444',borderRadius:4},\n    {label:'Medium',data:ALL_MONTHS.map(m=>ALL_DATA.filter(r=>fmtMonth(r.Date)===m&&r.AlertLevel==='MEDIUM').length),backgroundColor:'#f59e0b',borderRadius:4},\n    {label:'Low',data:ALL_MONTHS.map(m=>ALL_DATA.filter(r=>fmtMonth(r.Date)===m&&r.AlertLevel==='LOW').length),backgroundColor:'#22c55e',borderRadius:4}\n  ]);\n  const hTbl=document.getElementById('histTbl');\n  if(hTbl)hTbl.innerHTML=tblHTML(['Date','Student','Name','Class','Subject','Abs 30d','Rate','Trend','Alert'],\n    data.sort((a,b)=>parseDDMMYYYY(b.Date)-parseDDMMYYYY(a.Date)).map(r=>'<tr><td>'+r.Date+'<\\/td><td>'+r.StudentID+'<\\/td><td>'+(r.Name||'—')+'<\\/td><td>'+r.Class+'<\\/td><td>'+(r.Subject||'—')+'<\\/td><td>'+r.AbsencesLast30Days+'<\\/td><td><span style=\"color:'+pctColor(r.AttendancePct)+'\">'+parseFloat(r.AttendancePct).toFixed(1)+'%<\\/span><\\/td><td>'+trendBadge(r.Trend)+'<\\/td><td>'+alertBadge(r.AlertLevel)+'<\\/td><\\/tr>'));\n}\n\n// ── AT-RISK ──\nfunction renderRisk(){\n  const data=applyGlobalFilter(ALL_DATA);\n  // Use latest record per student\n  const latestBySid={};\n  data.forEach(r=>{if(!latestBySid[r.StudentID]||parseDDMMYYYY(r.Date)>parseDDMMYYYY(latestBySid[r.StudentID].Date))latestBySid[r.StudentID]=r;});\n  const latest=Object.values(latestBySid);\n  const high=latest.filter(r=>r.AlertLevel==='HIGH'),med=latest.filter(r=>r.AlertLevel==='MEDIUM');\n  const wors=latest.filter(r=>r.Trend==='WORSENING');\n  document.getElementById('riskKpis').innerHTML=kpiHTML([\n    {val:high.length,lbl:'High Risk Students',sub:'5+ abs or 3 consecutive',subc:'#64748b',vc:'#dc2626'},\n    {val:med.length,lbl:'Medium Risk',sub:'3–4 absences in 30d',subc:'#64748b',vc:'#d97706'},\n    {val:wors.length,lbl:'Worsening Trend',sub:'recent pattern up',subc:'#64748b',vc:'#d97706'},\n    {val:latest.filter(r=>+r.ConsecutiveStreak>=3).length,lbl:'3+ Day Streak',sub:'consecutive absences',subc:'#64748b',vc:'#dc2626'},\n    {val:latest.filter(r=>parseFloat(r.AttendancePct)<75).length,lbl:'Below 75%',sub:'attendance rate',subc:'#64748b',vc:'#dc2626'}\n  ]);\n  const rCols=['Student','Name','Class','Last Absent','Abs 30d','Streak','Rate','Trend'];\n  const rRow=r=>'<tr><td>'+r.StudentID+'<\\/td><td>'+(r.Name||'—')+'<\\/td><td>'+r.Class+'<\\/td><td>'+r.Date+'<\\/td><td><b style=\"color:#dc2626\">'+r.AbsencesLast30Days+'<\\/b><\\/td><td>'+(+r.ConsecutiveStreak>0?'<b style=\"color:#dc2626\">'+r.ConsecutiveStreak+'d<\\/b>':'—')+'<\\/td><td><span style=\"color:'+pctColor(r.AttendancePct)+'\">'+parseFloat(r.AttendancePct).toFixed(1)+'%<\\/span><\\/td><td>'+trendBadge(r.Trend)+'<\\/td><\\/tr>';\n  const hTbl=document.getElementById('highRiskTbl');if(hTbl)hTbl.innerHTML=tblHTML(rCols,high.sort((a,b)=>+b.AbsencesLast30Days-+a.AbsencesLast30Days).map(rRow));\n  const mTbl=document.getElementById('medRiskTbl');if(mTbl)mTbl.innerHTML=tblHTML(rCols,med.sort((a,b)=>+b.AbsencesLast30Days-+a.AbsencesLast30Days).map(rRow));\n  const wTbl=document.getElementById('worsenTbl');if(wTbl)wTbl.innerHTML=tblHTML(rCols,wors.sort((a,b)=>+b.AbsencesLast30Days-+a.AbsencesLast30Days).map(rRow));\n  // Top 10 most absent all time\n  const topAbs=Object.entries(STUDENT_STATS).sort((a,b)=>b[1].absent-a[1].absent).slice(0,10);\n  makeBar('topAbsentBar',topAbs.map(([sid,s])=>({label:s.name||sid,val:s.absent})),i=>{const r=i.val/Math.max(...topAbs.map(x=>x[1].absent),1);return r>0.7?'#ef4444':r>0.4?'#f59e0b':'#6366f1';});\n  // Subject risk chart\n  barChart('riskSubjectChart',ALL_SUBJECTS,[\n    {label:'High Risk',data:ALL_SUBJECTS.map(s=>high.filter(r=>r.Subject===s).length),backgroundColor:'#ef4444',borderRadius:4},\n    {label:'Medium Risk',data:ALL_SUBJECTS.map(s=>med.filter(r=>r.Subject===s).length),backgroundColor:'#f59e0b',borderRadius:4}\n  ]);\n}\n\nfunction kpiHTML(items){\n  return items.map(({val,lbl,sub,subc,vc})=>'<div class=\"kpi\"><div class=\"val\"'+(vc?' style=\"color:'+vc+'\"':'')+'>'+(val??0)+'<\\/div><div class=\"lbl\">'+lbl+'<\\/div>'+(sub?'<div class=\"sub\" style=\"color:'+(subc||'#64748b')+'\">'+sub+'<\\/div>':'')+'<\\/div>').join('');\n}\n\nfunction applyFilters(){\n  const active=document.querySelector('.tab.active');\n  if(active)active.click();\n}\n\nfunction switchTab(id,el){\n  document.querySelectorAll('.section').forEach(s=>s.classList.remove('active'));\n  document.querySelectorAll('.tab').forEach(t=>t.classList.remove('active'));\n  document.getElementById(id).classList.add('active');\n  el.classList.add('active');\n  setTimeout(()=>{\n    if(id==='today')renderToday();\n    else if(id==='weekly')renderWeekly();\n    else if(id==='monthly')renderMonthly();\n    else if(id==='history')renderHistory();\n    else if(id==='risk')renderRisk();\n  },30);\n}\n\n// Initial render\nrenderToday();\n<\\/script><\\/body><\\/html>`;\n\nreturn [\n  {json:{type:'csv', textContent:fullCsv,   fileName:'/home/node/.n8n-files/attendance_report.csv'}},\n  {json:{type:'html',textContent:dashHtml,  fileName:'/home/node/.n8n-files/dashboard.html'}}\n];\n"},"typeVersion":2},{"id":"1f31fd3e-db8e-42be-8611-da4c07a1d242","name":"Convert Data","type":"n8n-nodes-base.convertToFile","position":[3200,112],"parameters":{"options":{"fileName":"={{ $json.fileName }}"},"operation":"toText","sourceProperty":"textContent"},"typeVersion":1},{"id":"54ee6850-a6eb-47d4-bcc5-fa44250fc0a9","name":"Build Visual Report and Update Attendance File","type":"n8n-nodes-base.readWriteFile","position":[3456,112],"parameters":{"options":{},"fileName":"=/home/node/.n8n-files/{{ $binary.data.fileName }}","operation":"write"},"typeVersion":1},{"id":"56a26d48-b722-4c8a-bfa7-4d0825be0489","name":"Date Filter","type":"n8n-nodes-base.filter","position":[848,32],"parameters":{"options":{},"conditions":{"options":{"version":3,"leftValue":"","caseSensitive":true,"typeValidation":"strict"},"combinator":"and","conditions":[{"id":"a3daa56b-90fa-4921-8310-e203ba356284","operator":{"type":"string","operation":"equals"},"leftValue":"={{ $json.Date }}","rightValue":"={{ $now.toFormat('dd-MM-yyyy') }}"}]}},"typeVersion":2.3},{"id":"3913a364-a32c-4ed9-ac13-2095138266fc","name":"Group Dashboard Report","type":"n8n-nodes-base.stickyNote","position":[2144,0],"parameters":{"color":7,"width":1506,"height":324,"content":"## Dashboard & report update\n\nBuilds interactive HTML dashboard and appends today's records to attendance history CSV."},"typeVersion":1}],"active":false,"pinData":{},"settings":{"binaryMode":"separate","availableInMCP":false,"executionOrder":"v1"},"versionId":"8a9da86f-cf2b-4ffb-9b6e-f272d451e57e","connections":{"Date Filter":{"main":[[{"node":"IF: Is Absent?","type":"main","index":0}]]},"Convert Data":{"main":[[{"node":"Build Visual Report and Update Attendance File","type":"main","index":0}]]},"IF: Is Absent?":{"main":[[{"node":"Students Contacts","type":"main","index":0},{"node":"Merge Absent + Contacts","type":"main","index":0}],[{"node":"No Absences Today","type":"main","index":0}]]},"Alert Logic Block":{"main":[[{"node":"Generate Absence Email","type":"main","index":0},{"node":"Build Dashboard and Report","type":"main","index":0}]]},"No Absences Today":{"main":[[{"node":"Existing Report History","type":"main","index":0}]]},"Students Contacts":{"main":[[{"node":"Extract Contacts CSV","type":"main","index":0}]]},"Extract History CSV":{"main":[[{"node":"Combine History + Build Dashboard","type":"main","index":0}]]},"Extract Contacts CSV":{"main":[[{"node":"Merge Absent + Contacts","type":"main","index":1}]]},"Extract Attendance CSV":{"main":[[{"node":"Date Filter","type":"main","index":0}]]},"Generate Absence Email":{"main":[[{"node":"Send Email","type":"main","index":0}]]},"Recurring Time Trigger":{"main":[[{"node":"Student Attendance Data","type":"main","index":0}]]},"Existing Report History":{"main":[[{"node":"Extract History CSV","type":"main","index":0}]]},"Merge Absent + Contacts":{"main":[[{"node":"Alert Logic Block","type":"main","index":0}]]},"Student Attendance Data":{"main":[[{"node":"Extract Attendance CSV","type":"main","index":0}]]},"Build Dashboard and Report":{"main":[[{"node":"Existing Report History","type":"main","index":0}]]},"Combine History + Build Dashboard":{"main":[[{"node":"Convert Data","type":"main","index":0}]]}}},"lastUpdatedBy":1,"workflowInfo":{"nodeCount":25,"nodeTypes":{"n8n-nodes-base.if":{"count":1},"n8n-nodes-base.code":{"count":5},"n8n-nodes-base.merge":{"count":1},"n8n-nodes-base.filter":{"count":1},"n8n-nodes-base.emailSend":{"count":1},"n8n-nodes-base.stickyNote":{"count":7},"n8n-nodes-base.convertToFile":{"count":1},"n8n-nodes-base.readWriteFile":{"count":4},"n8n-nodes-base.scheduleTrigger":{"count":1},"n8n-nodes-base.spreadsheetFile":{"count":3}}},"status":"published","readyToDemo":null,"user":{"name":"Tejasv Makkar","username":"tmakkar","bio":"https://www.linkedin.com/in/tejasv-makkar/","verified":true,"links":[""],"avatar":"https://gravatar.com/avatar/0f31fdbbb21077b8977fa34ab0f71b60970572462574cede8ff1c0b8b4938003?r=pg&d=retro&size=200"},"nodes":[{"id":11,"icon":"fa:envelope","name":"n8n-nodes-base.emailSend","codex":{"data":{"alias":["SMTP","email","human","form","wait","hitl","approval"],"resources":{"generic":[{"url":"https://n8n.io/blog/2021-the-year-to-automate-the-new-you-with-n8n/","icon":"☀️","label":"2021: The Year to Automate the New You with n8n"},{"url":"https://n8n.io/blog/build-your-own-virtual-assistant-with-n8n-a-step-by-step-guide/","icon":"👦","label":"Build your own virtual assistant with n8n: A step by step guide"}],"primaryDocumentation":[{"url":"https://docs.n8n.io/integrations/builtin/core-nodes/n8n-nodes-base.sendemail/"}],"credentialDocumentation":[{"url":"https://docs.n8n.io/integrations/builtin/credentials/sendemail/"}]},"categories":["Communication","HITL","Core Nodes"],"nodeVersion":"1.0","codexVersion":"1.0","subcategories":{"HITL":["Human in the Loop"]}}},"group":"[\"output\"]","defaults":{"name":"Send Email","color":"#00bb88"},"iconData":{"icon":"envelope","type":"icon"},"displayName":"Send Email","typeVersion":2,"nodeCategories":[{"id":6,"name":"Communication"},{"id":9,"name":"Core Nodes"},{"id":28,"name":"HITL"}]},{"id":20,"icon":"fa:map-signs","name":"n8n-nodes-base.if","codex":{"data":{"alias":["Router","Filter","Condition","Logic","Boolean","Branch"],"details":"The IF node can be used to implement binary conditional logic in your workflow. You can set up one-to-many conditions to evaluate each item of data being inputted into the node. That data will either evaluate to TRUE or FALSE and route out of the node accordingly.\n\nThis node has multiple types of conditions: Bool, String, Number, and Date & Time.","resources":{"generic":[{"url":"https://n8n.io/blog/learn-to-automate-your-factorys-incident-reporting-a-step-by-step-guide/","icon":"🏭","label":"Learn to Automate Your Factory's Incident Reporting: A Step by Step Guide"},{"url":"https://n8n.io/blog/2021-the-year-to-automate-the-new-you-with-n8n/","icon":"☀️","label":"2021: The Year to Automate the New You with n8n"},{"url":"https://n8n.io/blog/why-business-process-automation-with-n8n-can-change-your-daily-life/","icon":"🧬","label":"Why business process automation with n8n can change your daily life"},{"url":"https://n8n.io/blog/create-a-toxic-language-detector-for-telegram/","icon":"🤬","label":"Create a toxic language detector for Telegram in 4 step"},{"url":"https://n8n.io/blog/no-code-ecommerce-workflow-automations/","icon":"store","label":"6 e-commerce workflows to power up your Shopify s"},{"url":"https://n8n.io/blog/how-to-build-a-low-code-self-hosted-url-shortener/","icon":"🔗","label":"How to build a low-code, self-hosted URL shortener in 3 steps"},{"url":"https://n8n.io/blog/automate-your-data-processing-pipeline-in-9-steps-with-n8n/","icon":"⚙️","label":"Automate your data processing pipeline in 9 steps"},{"url":"https://n8n.io/blog/how-to-get-started-with-crm-automation-and-no-code-workflow-ideas/","icon":"👥","label":"How to get started with CRM automation (with 3 no-code workflow ideas"},{"url":"https://n8n.io/blog/5-tasks-you-can-automate-with-notion-api/","icon":"⚡️","label":"5 tasks you can automate with the new Notion API "},{"url":"https://n8n.io/blog/automate-google-apps-for-productivity/","icon":"💡","label":"15 Google apps you can combine and automate to increase productivity"},{"url":"https://n8n.io/blog/automation-for-maintainers-of-open-source-projects/","icon":"🏷️","label":"How to automatically manage contributions to open-source projects"},{"url":"https://n8n.io/blog/how-uproc-scraped-a-multi-page-website-with-a-low-code-workflow/","icon":" 🕸️","label":"How uProc scraped a multi-page website with a low-code workflow"},{"url":"https://n8n.io/blog/5-workflow-automations-for-mattermost-that-we-love-at-n8n/","icon":"🤖","label":"5 workflow automations for Mattermost that we love at n8n"},{"url":"https://n8n.io/blog/why-this-product-manager-loves-workflow-automation-with-n8n/","icon":"🧠","label":"Why this Product Manager loves workflow automation with n8n"},{"url":"https://n8n.io/blog/sending-automated-congratulations-with-google-sheets-twilio-and-n8n/","icon":"🙌","label":"Sending Automated Congratulations with Google Sheets, Twilio, and n8n "},{"url":"https://n8n.io/blog/how-to-set-up-a-ci-cd-pipeline-with-no-code/","icon":"🎡","label":"How to set up a no-code CI/CD pipeline with GitHub and TravisCI"},{"url":"https://n8n.io/blog/benefits-of-automation-and-n8n-an-interview-with-hubspots-hugh-durkin/","icon":"🎖","label":"Benefits of automation and n8n: An interview with HubSpot's Hugh Durkin"},{"url":"https://n8n.io/blog/aws-workflow-automation/","label":"7 no-code workflow automations for Amazon Web Services"}],"primaryDocumentation":[{"url":"https://docs.n8n.io/integrations/builtin/core-nodes/n8n-nodes-base.if/"}]},"categories":["Core Nodes"],"nodeVersion":"1.0","codexVersion":"1.0","subcategories":{"Core Nodes":["Flow"]}}},"group":"[\"transform\"]","defaults":{"name":"If","color":"#408000"},"iconData":{"icon":"map-signs","type":"icon"},"displayName":"If","typeVersion":2,"nodeCategories":[{"id":9,"name":"Core Nodes"}]},{"id":24,"icon":"file:merge.svg","name":"n8n-nodes-base.merge","codex":{"data":{"alias":["Join","Concatenate","Wait"],"resources":{"generic":[{"url":"https://n8n.io/blog/how-to-sync-data-between-two-systems/","icon":"🏬","label":"How to synchronize data between two systems (one-way vs. two-way sync"},{"url":"https://n8n.io/blog/supercharging-your-conference-registration-process-with-n8n/","icon":"🎫","label":"Supercharging your conference registration process with n8n"},{"url":"https://n8n.io/blog/migrating-community-metrics-to-orbit-using-n8n/","icon":"📈","label":"Migrating Community Metrics to Orbit using n8n"},{"url":"https://n8n.io/blog/build-your-own-virtual-assistant-with-n8n-a-step-by-step-guide/","icon":"👦","label":"Build your own virtual assistant with n8n: A step by step guide"},{"url":"https://n8n.io/blog/sending-automated-congratulations-with-google-sheets-twilio-and-n8n/","icon":"🙌","label":"Sending Automated Congratulations with Google Sheets, Twilio, and n8n "},{"url":"https://n8n.io/blog/aws-workflow-automation/","label":"7 no-code workflow automations for Amazon Web Services"}],"primaryDocumentation":[{"url":"https://docs.n8n.io/integrations/builtin/core-nodes/n8n-nodes-base.merge/"}]},"categories":["Core Nodes"],"nodeVersion":"1.0","codexVersion":"1.0","subcategories":{"Core Nodes":["Flow","Data Transformation"]}}},"group":"[\"transform\"]","defaults":{"name":"Merge"},"iconData":{"type":"file","fileBuffer":"data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTEyIiBoZWlnaHQ9IjUxMiIgdmlld0JveD0iMCAwIDUxMiA1MTIiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxnIGNsaXAtcGF0aD0idXJsKCNjbGlwMF8xMTc3XzUxOCkiPgo8cGF0aCBmaWxsLXJ1bGU9ImV2ZW5vZGQiIGNsaXAtcnVsZT0iZXZlbm9kZCIgZD0iTTAgNDhDMCAyMS40OTAzIDIxLjQ5MDMgMCA0OCAwSDExMkMxMzguNTEgMCAxNjAgMjEuNDkwMyAxNjAgNDhWNTZIMTk2LjI1MkMyNDAuNDM1IDU2IDI3Ni4yNTIgOTEuODE3MiAyNzYuMjUyIDEzNlYxOTJDMjc2LjI1MiAyMTQuMDkxIDI5NC4xNjEgMjMyIDMxNi4yNTIgMjMySDM1MlYyMjRDMzUyIDE5Ny40OSAzNzMuNDkgMTc2IDQwMCAxNzZINDY0QzQ5MC41MSAxNzYgNTEyIDE5Ny40OSA1MTIgMjI0VjI4OEM1MTIgMzE0LjUxIDQ5MC41MSAzMzYgNDY0IDMzNkg0MDBDMzczLjQ5IDMzNiAzNTIgMzE0LjUxIDM1MiAyODhWMjgwSDMxNi4yNTJDMjk0LjE2MSAyODAgMjc2LjI1MiAyOTcuOTA5IDI3Ni4yNTIgMzIwVjM3NkMyNzYuMjUyIDQyMC4xODMgMjQwLjQzNSA0NTYgMTk2LjI1MiA0NTZIMTYwVjQ2NEMxNjAgNDkwLjUxIDEzOC41MSA1MTIgMTEyIDUxMkg0OEMyMS40OTAzIDUxMiAwIDQ5MC41MSAwIDQ2NFY0MDBDMCAzNzMuNDkgMjEuNDkwMyAzNTIgNDggMzUySDExMkMxMzguNTEgMzUyIDE2MCAzNzMuNDkgMTYwIDQwMFY0MDhIMTk2LjI1MkMyMTMuOTI1IDQwOCAyMjguMjUyIDM5My42NzMgMjI4LjI1MiAzNzZWMzIwQzIyOC4yNTIgMjk0Ljc4NCAyMzguODU5IDI3Mi4wNDQgMjU1Ljg1MyAyNTZDMjM4Ljg1OSAyMzkuOTU2IDIyOC4yNTIgMjE3LjIxNiAyMjguMjUyIDE5MlYxMzZDMjI4LjI1MiAxMTguMzI3IDIxMy45MjUgMTA0IDE5Ni4yNTIgMTA0SDE2MFYxMTJDMTYwIDEzOC41MSAxMzguNTEgMTYwIDExMiAxNjBINDhDMjEuNDkwMyAxNjAgMCAxMzguNTEgMCAxMTJWNDhaTTEwNCA0OEMxMDguNDE4IDQ4IDExMiA1MS41ODE3IDExMiA1NlYxMDRDMTEyIDEwOC40MTggMTA4LjQxOCAxMTIgMTA0IDExMkg1NkM1MS41ODE3IDExMiA0OCAxMDguNDE4IDQ4IDEwNFY1NkM0OCA1MS41ODE3IDUxLjU4MTcgNDggNTYgNDhIMTA0Wk00NTYgMjI0QzQ2MC40MTggMjI0IDQ2NCAyMjcuNTgyIDQ2NCAyMzJWMjgwQzQ2NCAyODQuNDE4IDQ2MC40MTggMjg4IDQ1NiAyODhINDA4QzQwMy41ODIgMjg4IDQwMCAyODQuNDE4IDQwMCAyODBWMjMyQzQwMCAyMjcuNTgyIDQwMy41ODIgMjI0IDQwOCAyMjRINDU2Wk0xMTIgNDA4QzExMiA0MDMuNTgyIDEwOC40MTggNDAwIDEwNCA0MDBINTZDNTEuNTgxNyA0MDAgNDggNDAzLjU4MiA0OCA0MDhWNDU2QzQ4IDQ2MC40MTggNTEuNTgxNyA0NjQgNTYgNDY0SDEwNEMxMDguNDE4IDQ2NCAxMTIgNDYwLjQxOCAxMTIgNDU2VjQwOFoiIGZpbGw9IiM1NEI4QzkiLz4KPC9nPgo8ZGVmcz4KPGNsaXBQYXRoIGlkPSJjbGlwMF8xMTc3XzUxOCI+CjxyZWN0IHdpZHRoPSI1MTIiIGhlaWdodD0iNTEyIiBmaWxsPSJ3aGl0ZSIvPgo8L2NsaXBQYXRoPgo8L2RlZnM+Cjwvc3ZnPgo="},"displayName":"Merge","typeVersion":3,"nodeCategories":[{"id":9,"name":"Core Nodes"}]},{"id":41,"icon":"fa:table","name":"n8n-nodes-base.spreadsheetFile","codex":{"data":{"alias":["_Excel","Excel","CSV","Sheet","Spreadsheet","xls","xlsx","ods"],"resources":{"generic":[{"url":"https://n8n.io/blog/build-your-own-virtual-assistant-with-n8n-a-step-by-step-guide/","icon":"👦","label":"Build your own virtual assistant with n8n: A step by step guide"}],"primaryDocumentation":[{"url":"https://docs.n8n.io/integrations/builtin/core-nodes/n8n-nodes-base.converttofile/"}]},"categories":["Data & Storage","Core Nodes"],"nodeVersion":"1.0","codexVersion":"1.0","subcategories":{"Core Nodes":["Files"]}}},"group":"[\"transform\"]","defaults":{"name":"Spreadsheet File","color":"#2244FF"},"iconData":{"icon":"table","type":"icon"},"displayName":"Spreadsheet File","typeVersion":2,"nodeCategories":[{"id":3,"name":"Data & Storage"},{"id":9,"name":"Core Nodes"}]},{"id":565,"icon":"fa:sticky-note","name":"n8n-nodes-base.stickyNote","codex":{"data":{"alias":["Comments","Notes","Sticky"],"categories":["Core Nodes"],"nodeVersion":"1.0","codexVersion":"1.0","subcategories":{"Core Nodes":["Helpers"]}}},"group":"[\"input\"]","defaults":{"name":"Sticky Note","color":"#FFD233"},"iconData":{"icon":"sticky-note","type":"icon"},"displayName":"Sticky Note","typeVersion":1,"nodeCategories":[{"id":9,"name":"Core Nodes"}]},{"id":834,"icon":"file:code.svg","name":"n8n-nodes-base.code","codex":{"data":{"alias":["cpde","Javascript","JS","Python","Script","Custom Code","Function"],"details":"The Code node allows you to execute JavaScript in your workflow.","resources":{"primaryDocumentation":[{"url":"https://docs.n8n.io/integrations/builtin/core-nodes/n8n-nodes-base.code/"}]},"categories":["Development","Core Nodes"],"nodeVersion":"1.0","codexVersion":"1.0","subcategories":{"Core Nodes":["Helpers","Data Transformation"]}}},"group":"[\"transform\"]","defaults":{"name":"Code"},"iconData":{"type":"file","fileBuffer":"data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTEyIiBoZWlnaHQ9IjUxMiIgdmlld0JveD0iMCAwIDUxMiA1MTIiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxnIGNsaXAtcGF0aD0idXJsKCNjbGlwMF8xMTcxXzQ0MSkiPgo8cGF0aCBkPSJNMTcwLjI4MyA0OEgxOTYuNUMyMDMuMTI3IDQ4IDIwOC41IDQyLjYyNzQgMjA4LjUgMzZWMTJDMjA4LjUgNS4zNzI1OCAyMDMuMTI3IDAgMTk2LjUgMEgxNzAuMjgzQzEyNi4xIDAgOTAuMjgzIDM1LjgxNzIgOTAuMjgzIDgwVjE3NkM5MC4yODMgMjA2LjkyOCA2NS4yMTA5IDIzMiAzNC4yODMgMjMySDIzQzE2LjM3MjYgMjMyIDExIDIzNy4zNzIgMTEgMjQ0VjI2OEMxMSAyNzQuNjI3IDE2LjM3MjQgMjgwIDIyLjk5OTYgMjgwTDM0LjI4MyAyODBDNjUuMjEwOSAyODAgOTAuMjgzIDMwNS4wNzIgOTAuMjgzIDMzNlY0NDBDOTAuMjgzIDQ3OS43NjQgMTIyLjUxOCA1MTIgMTYyLjI4MyA1MTJIMTk2LjVDMjAzLjEyNyA1MTIgMjA4LjUgNTA2LjYyNyAyMDguNSA1MDBWNDc2QzIwOC41IDQ2OS4zNzMgMjAzLjEyNyA0NjQgMTk2LjUgNDY0SDE2Mi4yODNDMTQ5LjAyOCA0NjQgMTM4LjI4MyA0NTMuMjU1IDEzOC4yODMgNDQwVjMzNkMxMzguMjgzIDMwOS4wMjIgMTI4LjAxMSAyODQuNDQzIDExMS4xNjQgMjY1Ljk2MUMxMDYuMTA5IDI2MC40MTYgMTA2LjEwOSAyNTEuNTg0IDExMS4xNjQgMjQ2LjAzOUMxMjguMDExIDIyNy41NTcgMTM4LjI4MyAyMDIuOTc4IDEzOC4yODMgMTc2VjgwQzEzOC4yODMgNjIuMzI2OSAxNTIuNjEgNDggMTcwLjI4MyA0OFoiIGZpbGw9IiNGRjk5MjIiLz4KPHBhdGggZD0iTTMwNSAzNkMzMDUgNDIuNjI3NCAzMTAuMzczIDQ4IDMxNyA0OEgzNDIuOTc5QzM2MC42NTIgNDggMzc0Ljk3OCA2Mi4zMjY5IDM3NC45NzggODBWMTc2QzM3NC45NzggMjAyLjk3OCAzODUuMjUxIDIyNy41NTcgNDAyLjA5OCAyNDYuMDM5QzQwNy4xNTMgMjUxLjU4NCA0MDcuMTUzIDI2MC40MTYgNDAyLjA5OCAyNjUuOTYxQzM4NS4yNTEgMjg0LjQ0MyAzNzQuOTc4IDMwOS4wMjIgMzc0Ljk3OCAzMzZWNDMyQzM3NC45NzggNDQ5LjY3MyAzNjAuNjUyIDQ2NCAzNDIuOTc5IDQ2NEgzMTdDMzEwLjM3MyA0NjQgMzA1IDQ2OS4zNzMgMzA1IDQ3NlY1MDBDMzA1IDUwNi42MjcgMzEwLjM3MyA1MTIgMzE3IDUxMkgzNDIuOTc5QzM4Ny4xNjEgNTEyIDQyMi45NzggNDc2LjE4MyA0MjIuOTc4IDQzMlYzMzZDNDIyLjk3OCAzMDUuMDcyIDQ0OC4wNTEgMjgwIDQ3OC45NzkgMjgwSDQ5MEM0OTYuNjI3IDI4MCA1MDIgMjc0LjYyOCA1MDIgMjY4VjI0NEM1MDIgMjM3LjM3MyA0OTYuNjI4IDIzMiA0OTAgMjMyTDQ3OC45NzkgMjMyQzQ0OC4wNTEgMjMyIDQyMi45NzggMjA2LjkyOCA0MjIuOTc4IDE3NlY4MEM0MjIuOTc4IDM1LjgxNzIgMzg3LjE2MSAwIDM0Mi45NzkgMEgzMTdDMzEwLjM3MyAwIDMwNSA1LjM3MjU4IDMwNSAxMlYzNloiIGZpbGw9IiNGRjk5MjIiLz4KPC9nPgo8ZGVmcz4KPGNsaXBQYXRoIGlkPSJjbGlwMF8xMTcxXzQ0MSI+CjxyZWN0IHdpZHRoPSI1MTIiIGhlaWdodD0iNTEyIiBmaWxsPSJ3aGl0ZSIvPgo8L2NsaXBQYXRoPgo8L2RlZnM+Cjwvc3ZnPgo="},"displayName":"Code","typeVersion":2,"nodeCategories":[{"id":5,"name":"Development"},{"id":9,"name":"Core Nodes"}]},{"id":839,"icon":"fa:clock","name":"n8n-nodes-base.scheduleTrigger","codex":{"data":{"alias":["Time","Scheduler","Polling","Cron","Interval"],"resources":{"generic":[],"primaryDocumentation":[{"url":"https://docs.n8n.io/integrations/builtin/core-nodes/n8n-nodes-base.scheduletrigger/"}]},"categories":["Core Nodes"],"nodeVersion":"1.0","codexVersion":"1.0"}},"group":"[\"trigger\",\"schedule\"]","defaults":{"name":"Schedule Trigger","color":"#31C49F"},"iconData":{"icon":"clock","type":"icon"},"displayName":"Schedule Trigger","typeVersion":1,"nodeCategories":[{"id":9,"name":"Core Nodes"}]},{"id":844,"icon":"fa:filter","name":"n8n-nodes-base.filter","codex":{"data":{"alias":["Router","Filter","Condition","Logic","Boolean","Branch"],"details":"The Filter node can be used to filter items based on a condition. If the condition is met, the item will be passed on to the next node. If the condition is not met, the item will be omitted. Conditions can be combined together by AND(meet all conditions), or OR(meet at least one condition).","resources":{"generic":[],"primaryDocumentation":[{"url":"https://docs.n8n.io/integrations/builtin/core-nodes/n8n-nodes-base.filter/"}]},"categories":["Core Nodes"],"nodeVersion":"1.0","codexVersion":"1.0","subcategories":{"Core Nodes":["Flow","Data Transformation"]}}},"group":"[\"transform\"]","defaults":{"name":"Filter","color":"#229eff"},"iconData":{"icon":"filter","type":"icon"},"displayName":"Filter","typeVersion":2,"nodeCategories":[{"id":9,"name":"Core Nodes"}]},{"id":1233,"icon":"file:readWriteFile.svg","name":"n8n-nodes-base.readWriteFile","codex":{"data":{"alias":["Binary","Binary File","File","Text","Open","Import","Save","Export","Disk","Transfer","Read Binary File","Write Binary File"],"resources":{"primaryDocumentation":[{"url":"https://docs.n8n.io/integrations/builtin/core-nodes/n8n-nodes-base.readwritefile/"}]},"categories":["Core Nodes"],"nodeVersion":"1.0","codexVersion":"1.0","subcategories":{"Core Nodes":["Files"]}}},"group":"[\"input\"]","defaults":{"name":"Read/Write Files from Disk"},"iconData":{"type":"file","fileBuffer":"data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTEyIiBoZWlnaHQ9IjUxMiIgdmlld0JveD0iMCAwIDUxMiA1MTIiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxnIGNsaXAtcGF0aD0idXJsKCNjbGlwMF8xMTQxXzE1NDcpIj4KPHBhdGggZD0iTTAgMTJDMCA1LjM3MjU4IDUuMzcyNTggMCAxMiAwSDE1OVYxNTRDMTU5IDE2MC42MjcgMTY0LjM3MyAxNjYgMTcxIDE2NkgzMjVWMjQySDIyOC41NjJDMjEwLjg5NSAyNDIgMTk0LjY1NiAyNTEuNzA1IDE4Ni4yODggMjY3LjI2NEwxMjkuMjAzIDM3My40MDdDMTI1LjEzMSAzODAuOTc4IDEyMyAzODkuNDQgMTIzIDM5OC4wMzdWNDM0SDEyQzUuMzcyNTcgNDM0IDAgNDI4LjYyNyAwIDQyMlYxMloiIGZpbGw9IiM0NEFBNDQiLz4KPHBhdGggZD0iTTMyNSAxMzRWMTI3LjQwMUMzMjUgMTI0LjIyMyAzMjMuNzQgMTIxLjE3NSAzMjEuNDk1IDExOC45MjVMMjA2LjM2OSAzLjUyNDgxQzIwNC4xMTggMS4yNjgyIDIwMS4wNjEgMCAxOTcuODczIDBIMTkxVjEzNEgzMjVaIiBmaWxsPSIjNDRBQTQ0Ii8+CjxwYXRoIGZpbGwtcnVsZT0iZXZlbm9kZCIgY2xpcC1ydWxlPSJldmVub2RkIiBkPSJNMjI4LjU2MyAyNzRDMjIyLjY3NCAyNzQgMjE3LjI2MSAyNzcuMjM1IDIxNC40NzIgMjgyLjQyMUwxNzIuMjExIDM2MUg0OTIuNjRMNDQ0LjY3IDI4MS43MTdDNDQxLjc3MiAyNzYuOTI3IDQzNi41OCAyNzQgNDMwLjk4MSAyNzRIMjI4LjU2M1oiIGZpbGw9IiM0NEFBNDQiLz4KPHBhdGggZmlsbC1ydWxlPSJldmVub2RkIiBjbGlwLXJ1bGU9ImV2ZW5vZGQiIGQ9Ik0xNTUgNDA5QzE1NSA0MDAuMTYzIDE2Mi4xNjMgMzkzIDE3MSAzOTNINDk2QzUwNC44MzcgMzkzIDUxMiA0MDAuMTYzIDUxMiA0MDlWNDk2QzUxMiA1MDQuODM3IDUwNC44MzcgNTEyIDQ5NiA1MTJIMTcxQzE2Mi4xNjMgNTEyIDE1NSA1MDQuODM3IDE1NSA0OTZWNDA5Wk0zOTcgNDUzQzM5NyA0NjYuMjU1IDM4Ni4yNTUgNDc3IDM3MyA0NzdDMzU5Ljc0NSA0NzcgMzQ5IDQ2Ni4yNTUgMzQ5IDQ1M0MzNDkgNDM5Ljc0NSAzNTkuNzQ1IDQyOSAzNzMgNDI5QzM4Ni4yNTUgNDI5IDM5NyA0MzkuNzQ1IDM5NyA0NTNaTTQ0NSA0NzdDNDU4LjI1NSA0NzcgNDY5IDQ2Ni4yNTUgNDY5IDQ1M0M0NjkgNDM5Ljc0NSA0NTguMjU1IDQyOSA0NDUgNDI5QzQzMS43NDUgNDI5IDQyMSA0MzkuNzQ1IDQyMSA0NTNDNDIxIDQ2Ni4yNTUgNDMxLjc0NSA0NzcgNDQ1IDQ3N1oiIGZpbGw9IiM0NEFBNDQiLz4KPC9nPgo8ZGVmcz4KPGNsaXBQYXRoIGlkPSJjbGlwMF8xMTQxXzE1NDciPgo8cmVjdCB3aWR0aD0iNTEyIiBoZWlnaHQ9IjUxMiIgZmlsbD0id2hpdGUiLz4KPC9jbGlwUGF0aD4KPC9kZWZzPgo8L3N2Zz4K"},"displayName":"Read/Write Files from Disk","typeVersion":1,"nodeCategories":[{"id":9,"name":"Core Nodes"}]},{"id":1234,"icon":"file:convertToFile.svg","name":"n8n-nodes-base.convertToFile","codex":{"data":{"alias":["CSV","Spreadsheet","Excel","xls","xlsx","ods","tabular","encode","encoding","Move Binary Data","Binary","File","JSON","HTML","ICS","iCal","RTF","64","Base64"],"resources":{"primaryDocumentation":[{"url":"https://docs.n8n.io/integrations/builtin/core-nodes/n8n-nodes-base.converttofile/"}]},"categories":["Core Nodes"],"nodeVersion":"1.0","codexVersion":"1.0","subcategories":{"Core Nodes":["Files","Data Transformation"]}}},"group":"[\"input\"]","defaults":{"name":"Convert to File"},"iconData":{"type":"file","fileBuffer":"data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNDAiIGhlaWdodD0iNDAiIHZpZXdCb3g9IjAgMCA0MCA0MCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTEzLjc2MTkgMkMxMy4yNDM3IDIgMTIuODIzNiAyLjQyMDA5IDEyLjgyMzYgMi45MzgzMVYxNS4yNTI2QzEzLjMxOTkgMTUuNDY0MyAxMy43ODUxIDE1Ljc3MiAxNC4xOTEgMTYuMTc1N0wyMS4yMjgzIDIzLjE3MzlDMjIuMDU0OCAyMy45OTU4IDIyLjUxOTUgMjUuMTEzMiAyMi41MTk1IDI2LjI3ODhDMjIuNTE5NSAyNy40NDQzIDIyLjA1NDggMjguNTYxOCAyMS4yMjgzIDI5LjM4MzdMMTQuMTkxIDM2LjM4MTlDMTMuNzg1IDM2Ljc4NTYgMTMuMzE5OSAzNy4wOTMyIDEyLjgyMzYgMzcuMzA1VjM3LjM1MjdDMTIuODIzNiAzNy44NzA5IDEzLjI0MzcgMzguMjkxIDEzLjc2MTkgMzguMjkxSDM5LjA2MTdDMzkuNTc5OSAzOC4yOTEgNDAgMzcuODcwOSA0MCAzNy4zNTI3TDQwIDE1Ljc5NEgyNy4xNDQzQzI2LjYyNjEgMTUuNzk0IDI2LjIwNiAxNS4zNzM5IDI2LjIwNiAxNC44NTU3VjJIMTMuNzYxOVoiIGZpbGw9IiMzQTQyRTkiLz4KPHBhdGggZD0iTTI4Ljg2NDUgMkMyOC43NzgxIDIgMjguNzA4MSAyLjA3MDAyIDI4LjcwODEgMi4xNTYzOVYxMi44MjI3QzI4LjcwODEgMTMuMDgxOCAyOC45MTgyIDEzLjI5MTkgMjkuMTc3MyAxMy4yOTE5SDM5Ljg0MzZDMzkuOTMgMTMuMjkxOSA0MCAxMy4yMjE5IDQwIDEzLjEzNTVMNDAgMTIuNjI2M0M0MCAxMi4zNzc4IDM5LjkwMTQgMTIuMTM5NSAzOS43MjYgMTEuOTYzNkwzMC4wNjEgMi4yNzU2MUMyOS44ODUgMi4wOTkxNiAyOS42NDYgMiAyOS4zOTY3IDJIMjguODY0NVoiIGZpbGw9IiMzQTQyRTkiLz4KPHBhdGggZD0iTTkuNzcyNjggMzQuNjAwM0M5LjA0MTg2IDMzLjg2NTQgOS4wNDUxNyAzMi42NzcyIDkuNzgwMDcgMzEuOTQ2NEwxMy42MzE1IDI4LjExNjNMMC45MzgzMTEgMjguMTE2M0MwLjQyMDA5NiAyOC4xMTYzIC0yLjI2NTE5ZS0wOCAyNy42OTYyIDAgMjcuMTc4TDguMjAyOTdlLTA4IDI1LjMwMTRDMS4wNDY4MmUtMDcgMjQuNzgzMiAwLjQyMDA5NSAyNC4zNjMxIDAuOTM4MzExIDI0LjM2MzFIMTMuNTUyOUw5Ljc4MDA3IDIwLjYxMTJDOS4wNDUxNyAxOS44ODA0IDkuMDQxODYgMTguNjkyMiA5Ljc3MjY4IDE3Ljk1NzNDMTAuNTAzNSAxNy4yMjI0IDExLjY5MTcgMTcuMjE5MSAxMi40MjY2IDE3Ljk0OTlMMTkuNDYzOSAyNC45NDgxQzE5LjgxODEgMjUuMzAwNCAyMC4wMTczIDI1Ljc3OTMgMjAuMDE3MyAyNi4yNzg4QzIwLjAxNzMgMjYuNzc4MyAxOS44MTgxIDI3LjI1NzIgMTkuNDYzOSAyNy42MDk1TDEyLjQyNjYgMzQuNjA3N0MxMS42OTE3IDM1LjMzODUgMTAuNTAzNSAzNS4zMzUyIDkuNzcyNjggMzQuNjAwM1oiIGZpbGw9IiMzQTQyRTkiLz4KPC9zdmc+Cg=="},"displayName":"Convert to File","typeVersion":1,"nodeCategories":[{"id":9,"name":"Core Nodes"}]}],"categories":[{"id":45,"name":"Miscellaneous"},{"id":49,"name":"AI Summarization"}],"image":[]}}