This workflow follows the Emailsend → HTTP Request recipe pattern — see all workflows that pair these two integrations.
The workflow JSON
Copy or download the full n8n JSON below. Paste it into a new n8n workflow, add your credentials, activate. Full import guide →
{
"name": "WF-008 Monthly Report",
"nodes": [
{
"parameters": {
"rule": {
"interval": [
{
"field": "cronExpression",
"expression": "0 9 1 * *"
}
]
}
},
"id": "wf008-node-schedule",
"name": "1st of Month 9AM ET",
"type": "n8n-nodes-base.scheduleTrigger",
"typeVersion": 1.2,
"position": [
0,
300
]
},
{
"parameters": {
"operation": "getAll",
"tableId": "tenants",
"returnAll": true,
"filterType": "string",
"filterString": "subscription_status=eq.active"
},
"id": "wf008-node-get-tenants",
"name": "Get Active Tenants",
"type": "n8n-nodes-base.supabase",
"typeVersion": 1,
"position": [
250,
300
],
"credentials": {
"supabaseApi": {
"name": "<your credential>"
}
}
},
{
"parameters": {
"batchSize": 1,
"options": {}
},
"id": "wf008-node-loop",
"name": "Loop Over Tenants",
"type": "n8n-nodes-base.splitInBatches",
"typeVersion": 3,
"position": [
500,
300
]
},
{
"parameters": {
"method": "GET",
"url": "https://yepggeqhtspgzlkhqhgo.supabase.co/rest/v1/usage_monthly",
"authentication": "predefinedCredentialType",
"nodeCredentialType": "supabaseApi",
"sendQuery": true,
"queryParameters": {
"parameters": [
{
"name": "tenant_id",
"value": "={{ 'eq.' + $('Loop Over Tenants').item.json.id }}"
},
{
"name": "year_month",
"value": "={{ (() => { const d = new Date(); d.setMonth(d.getMonth() - 1); return 'eq.' + d.getFullYear() + '-' + String(d.getMonth() + 1).padStart(2, '0'); })() }}"
},
{
"name": "select",
"value": "*"
}
]
}
},
"id": "wf008-node-get-monthly",
"name": "Get Monthly Usage",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
750,
300
],
"credentials": {
"supabaseApi": {
"name": "<your credential>"
}
}
},
{
"parameters": {
"method": "GET",
"url": "https://yepggeqhtspgzlkhqhgo.supabase.co/rest/v1/usage_daily",
"authentication": "predefinedCredentialType",
"nodeCredentialType": "supabaseApi",
"sendQuery": true,
"queryParameters": {
"parameters": [
{
"name": "tenant_id",
"value": "={{ 'eq.' + $('Loop Over Tenants').item.json.id }}"
},
{
"name": "date",
"value": "={{ (() => { const d = new Date(); d.setMonth(d.getMonth() - 1); const start = d.getFullYear() + '-' + String(d.getMonth() + 1).padStart(2, '0') + '-01'; const end = new Date(d.getFullYear(), d.getMonth() + 1, 0).toISOString().split('T')[0]; return 'gte.' + start + '&date=lte.' + end; })() }}"
},
{
"name": "select",
"value": "*"
},
{
"name": "order",
"value": "date.asc"
}
]
}
},
"id": "wf008-node-get-daily",
"name": "Get Daily Trends",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
1000,
300
],
"credentials": {
"supabaseApi": {
"name": "<your credential>"
}
}
},
{
"parameters": {
"jsCode": "// Compute monthly insights from daily and monthly data\nconst dailyItems = $input.all();\nconst monthlyData = $('Get Monthly Usage').first().json || {};\nconst tenant = $('Loop Over Tenants').item.json;\n\nconst dailyRecords = dailyItems.map(i => i.json).filter(d => d.date);\n\n// Previous month label\nconst prevDate = new Date();\nprevDate.setMonth(prevDate.getMonth() - 1);\nconst monthLabel = prevDate.toLocaleDateString('en-US', { year: 'numeric', month: 'long' });\n\n// Basic stats from monthly summary\nconst totalCalls = monthlyData.total_calls || 0;\nconst totalMinutes = monthlyData.total_minutes || 0;\nconst answered = monthlyData.answered || 0;\nconst missed = monthlyData.missed || 0;\nconst voicemail = monthlyData.voicemail || 0;\nconst escalations = monthlyData.escalations || 0;\nconst daysInMonth = dailyRecords.length || 1;\n\n// Computed insights\nconst avgCallsPerDay = Math.round(totalCalls / daysInMonth * 10) / 10;\nconst answerRate = totalCalls > 0 ? Math.round((answered / totalCalls) * 100) : 0;\nconst missRate = totalCalls > 0 ? Math.round((missed / totalCalls) * 100) : 0;\nconst escalationRate = totalCalls > 0 ? Math.round((escalations / totalCalls) * 100) : 0;\n\n// Busiest day\nlet busiestDay = { date: 'N/A', calls: 0 };\nfor (const day of dailyRecords) {\n if ((day.total_calls || 0) > busiestDay.calls) {\n busiestDay = {\n date: new Date(day.date).toLocaleDateString('en-US', { weekday: 'long', month: 'short', day: 'numeric' }),\n calls: day.total_calls\n };\n }\n}\n\n// Time & cost savings (1.5x multiplier, $25/hr)\nconst totalHours = Math.round(totalMinutes / 60 * 100) / 100;\nconst hoursSaved = Math.round(totalHours * 1.5 * 100) / 100;\nconst laborCostSaved = Math.round(hoursSaved * 25 * 100) / 100;\n\n// Resolution rate (answered without escalation)\nconst resolvedWithoutEscalation = answered - escalations;\nconst resolutionRate = answered > 0 ? Math.round((resolvedWithoutEscalation / answered) * 100) : 0;\n\n// Build insights array for ai_insights table\nconst insights = [\n {\n tenant_id: tenant.id,\n category: 'trend',\n severity: 'info',\n title: 'Monthly Call Volume',\n message: `${totalCalls} total calls handled in ${monthLabel}, averaging ${avgCallsPerDay} calls per day.`\n },\n {\n tenant_id: tenant.id,\n category: 'trend',\n severity: 'info',\n title: 'Busiest Day',\n message: `${busiestDay.date} was the busiest day with ${busiestDay.calls} calls.`\n },\n {\n tenant_id: tenant.id,\n category: 'trend',\n severity: 'info',\n title: 'Answer Rate',\n message: `${answerRate}% answer rate with ${missed} missed calls and ${voicemail} voicemails.`\n },\n {\n tenant_id: tenant.id,\n category: 'recommendation',\n severity: 'info',\n title: 'Time & Cost Savings',\n message: `TitleFlow AI saved an estimated ${hoursSaved} staff hours ($${laborCostSaved.toLocaleString()} in labor costs) at a 1.5x efficiency multiplier.`\n },\n {\n tenant_id: tenant.id,\n category: 'trend',\n severity: 'info',\n title: 'Resolution Rate',\n message: `${resolutionRate}% of answered calls resolved without escalation.`\n }\n];\n\nif (escalationRate > 15) {\n insights.push({\n tenant_id: tenant.id,\n category: 'recommendation',\n severity: 'info',\n title: 'High Escalation Rate',\n message: `Escalation rate was ${escalationRate}%. Consider reviewing AI call scripts to reduce escalations.`\n });\n}\n\nreturn [\n {\n json: {\n tenant_id: tenant.id,\n company_name: tenant.company_name || 'Customer',\n owner_email: tenant.owner_email || tenant.contact_email || '',\n plan: tenant.plan || 'Standard',\n month_label: monthLabel,\n total_calls: totalCalls,\n total_minutes: totalMinutes,\n total_hours: totalHours,\n answered: answered,\n missed: missed,\n voicemail: voicemail,\n escalations: escalations,\n avg_calls_per_day: avgCallsPerDay,\n answer_rate: answerRate,\n miss_rate: missRate,\n escalation_rate: escalationRate,\n resolution_rate: resolutionRate,\n busiest_day: busiestDay.date,\n busiest_day_calls: busiestDay.calls,\n hours_saved: hoursSaved,\n labor_cost_saved: laborCostSaved,\n insights: insights\n }\n }\n];"
},
"id": "wf008-node-compute-insights",
"name": "Compute Insights",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
1250,
300
]
},
{
"parameters": {
"jsCode": "// Insert each insight as a separate item for the Supabase upsert\nconst data = $input.all()[0].json;\nconst insights = data.insights || [];\n\nreturn insights.map(insight => ({\n json: {\n tenant_id: insight.tenant_id,\n category: insight.category,\n severity: insight.severity,\n title: insight.title,\n message: insight.message,\n month: data.month_label,\n created_at: new Date().toISOString(),\n // Pass through report data for downstream nodes\n _report: {\n tenant_id: data.tenant_id,\n company_name: data.company_name,\n owner_email: data.owner_email,\n plan: data.plan,\n month_label: data.month_label,\n total_calls: data.total_calls,\n total_minutes: data.total_minutes,\n total_hours: data.total_hours,\n answered: data.answered,\n missed: data.missed,\n voicemail: data.voicemail,\n escalations: data.escalations,\n avg_calls_per_day: data.avg_calls_per_day,\n answer_rate: data.answer_rate,\n miss_rate: data.miss_rate,\n escalation_rate: data.escalation_rate,\n resolution_rate: data.resolution_rate,\n busiest_day: data.busiest_day,\n busiest_day_calls: data.busiest_day_calls,\n hours_saved: data.hours_saved,\n labor_cost_saved: data.labor_cost_saved\n }\n }\n}));"
},
"id": "wf008-node-split-insights",
"name": "Prepare Insight Records",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
1500,
300
]
},
{
"parameters": {
"operation": "create",
"tableId": "ai_insights",
"dataToSend": "defineBelow",
"fieldsUi": {
"fieldValues": [
{
"fieldName": "tenant_id",
"fieldValue": "={{ $json.tenant_id }}"
},
{
"fieldName": "category",
"fieldValue": "={{ $json.category }}"
},
{
"fieldName": "severity",
"fieldValue": "={{ $json.severity }}"
},
{
"fieldName": "title",
"fieldValue": "={{ $json.title }}"
},
{
"fieldName": "message",
"fieldValue": "={{ $json.message }}"
}
]
}
},
"id": "wf008-node-insert-insights",
"name": "Insert AI Insights",
"type": "n8n-nodes-base.supabase",
"typeVersion": 1,
"position": [
1750,
300
],
"credentials": {
"supabaseApi": {
"name": "<your credential>"
}
}
},
{
"parameters": {
"jsCode": "// Build HTML monthly report email from the report data\nconst items = $input.all();\n// Get the report data from the last item (all items carry the same _report)\nconst report = items[items.length - 1].json._report || items[0].json._report || {};\n\nconst r = report;\nconst monthLabel = r.month_label || 'Last Month';\n\nconst subject = `Your TitleFlow AI Monthly Report - ${monthLabel}`;\n\nconst htmlBody = `\n<div style=\"font-family: 'Inter', Arial, sans-serif; max-width: 640px; margin: 0 auto; background: #000000; color: #ffffff; border-radius: 16px; overflow: hidden;\">\n <!-- Header -->\n <div style=\"background: linear-gradient(135deg, #0080FF, #4F1AD6); padding: 40px 32px; text-align: center;\">\n <h1 style=\"margin: 0; font-size: 28px; font-weight: 700;\">TitleFlow AI</h1>\n <p style=\"margin: 8px 0 0; opacity: 0.9; font-size: 16px;\">Monthly Performance Report</p>\n <p style=\"margin: 4px 0 0; opacity: 0.7; font-size: 14px;\">${monthLabel}</p>\n </div>\n \n <!-- Summary Stats -->\n <div style=\"padding: 32px;\">\n <p style=\"font-size: 16px; line-height: 1.6; margin-bottom: 24px;\">Hi <strong>${r.company_name}</strong>, here is your monthly performance summary:</p>\n \n <div style=\"display: flex; flex-wrap: wrap; gap: 12px; margin-bottom: 24px;\">\n <div style=\"flex: 1; min-width: 120px; background: rgba(0, 128, 255, 0.1); border: 1px solid rgba(0, 128, 255, 0.2); border-radius: 12px; padding: 16px; text-align: center;\">\n <div style=\"font-size: 28px; font-weight: 700; color: #00D9FF;\">${r.total_calls}</div>\n <div style=\"font-size: 12px; color: #999999; margin-top: 4px;\">Total Calls</div>\n </div>\n <div style=\"flex: 1; min-width: 120px; background: rgba(0, 128, 255, 0.1); border: 1px solid rgba(0, 128, 255, 0.2); border-radius: 12px; padding: 16px; text-align: center;\">\n <div style=\"font-size: 28px; font-weight: 700; color: #00D9FF;\">${r.total_hours}h</div>\n <div style=\"font-size: 12px; color: #999999; margin-top: 4px;\">Total Hours</div>\n </div>\n <div style=\"flex: 1; min-width: 120px; background: rgba(0, 128, 255, 0.1); border: 1px solid rgba(0, 128, 255, 0.2); border-radius: 12px; padding: 16px; text-align: center;\">\n <div style=\"font-size: 28px; font-weight: 700; color: #00D9FF;\">${r.answer_rate}%</div>\n <div style=\"font-size: 12px; color: #999999; margin-top: 4px;\">Answer Rate</div>\n </div>\n </div>\n \n <!-- Detailed Breakdown -->\n <div style=\"background: rgba(255,255,255,0.03); border: 1px solid rgba(255,255,255,0.08); border-radius: 12px; padding: 20px; margin-bottom: 24px;\">\n <h3 style=\"margin: 0 0 16px; font-size: 16px; color: #ffffff;\">Call Breakdown</h3>\n <table style=\"width: 100%; border-collapse: collapse; font-size: 14px;\">\n <tr style=\"border-bottom: 1px solid rgba(255,255,255,0.06);\">\n <td style=\"padding: 10px 0; color: #999999;\">Answered</td>\n <td style=\"padding: 10px 0; text-align: right; font-weight: 600; color: #4ADE80;\">${r.answered}</td>\n </tr>\n <tr style=\"border-bottom: 1px solid rgba(255,255,255,0.06);\">\n <td style=\"padding: 10px 0; color: #999999;\">Missed</td>\n <td style=\"padding: 10px 0; text-align: right; font-weight: 600; color: #FF6B6B;\">${r.missed}</td>\n </tr>\n <tr style=\"border-bottom: 1px solid rgba(255,255,255,0.06);\">\n <td style=\"padding: 10px 0; color: #999999;\">Voicemail</td>\n <td style=\"padding: 10px 0; text-align: right; font-weight: 600; color: #FFB800;\">${r.voicemail}</td>\n </tr>\n <tr style=\"border-bottom: 1px solid rgba(255,255,255,0.06);\">\n <td style=\"padding: 10px 0; color: #999999;\">Escalations</td>\n <td style=\"padding: 10px 0; text-align: right; font-weight: 600; color: #FF6B6B;\">${r.escalations}</td>\n </tr>\n <tr style=\"border-bottom: 1px solid rgba(255,255,255,0.06);\">\n <td style=\"padding: 10px 0; color: #999999;\">Avg Calls/Day</td>\n <td style=\"padding: 10px 0; text-align: right; font-weight: 600; color: #00D9FF;\">${r.avg_calls_per_day}</td>\n </tr>\n <tr>\n <td style=\"padding: 10px 0; color: #999999;\">Resolution Rate</td>\n <td style=\"padding: 10px 0; text-align: right; font-weight: 600; color: #4ADE80;\">${r.resolution_rate}%</td>\n </tr>\n </table>\n </div>\n \n <!-- Key Insight -->\n <div style=\"background: rgba(255,255,255,0.03); border: 1px solid rgba(255,255,255,0.08); border-radius: 12px; padding: 20px; margin-bottom: 24px;\">\n <h3 style=\"margin: 0 0 12px; font-size: 16px; color: #ffffff;\">Key Insights</h3>\n <p style=\"margin: 0 0 8px; font-size: 14px; color: #cccccc; line-height: 1.6;\"><span style=\"color: #00D9FF;\">●</span> Busiest day: <strong>${r.busiest_day}</strong> with ${r.busiest_day_calls} calls</p>\n <p style=\"margin: 0 0 8px; font-size: 14px; color: #cccccc; line-height: 1.6;\"><span style=\"color: #00D9FF;\">●</span> ${r.resolution_rate}% of calls resolved without human escalation</p>\n <p style=\"margin: 0; font-size: 14px; color: #cccccc; line-height: 1.6;\"><span style=\"color: #00D9FF;\">●</span> ${r.escalation_rate}% escalation rate</p>\n </div>\n \n <!-- Savings -->\n <div style=\"background: linear-gradient(135deg, rgba(0, 128, 255, 0.15), rgba(79, 26, 214, 0.15)); border: 1px solid rgba(0, 217, 255, 0.2); border-radius: 12px; padding: 24px; margin-bottom: 24px; text-align: center;\">\n <p style=\"margin: 0 0 4px; font-size: 12px; color: #999999; text-transform: uppercase; letter-spacing: 1px;\">Estimated Savings This Month</p>\n <div style=\"font-size: 36px; font-weight: 700; color: #4ADE80;\">$${r.labor_cost_saved.toLocaleString()}</div>\n <p style=\"margin: 8px 0 0; font-size: 14px; color: #cccccc;\">${r.hours_saved} staff hours saved at 1.5x efficiency × $25/hr</p>\n </div>\n \n <div style=\"text-align: center; margin-top: 24px;\">\n <a href=\"https://www.titleflowai.ai/dashboard\" style=\"display: inline-block; background: linear-gradient(135deg, #0080FF, #4F1AD6); color: #ffffff; text-decoration: none; padding: 14px 36px; border-radius: 999px; font-weight: 600; font-size: 14px;\">View Full Dashboard</a>\n </div>\n </div>\n \n <!-- Footer -->\n <div style=\"padding: 20px 32px; text-align: center; font-size: 12px; color: #666666; border-top: 1px solid rgba(255,255,255,0.05);\">\n <p style=\"margin: 0 0 4px;\">TitleFlow AI AI — Automating title industry communications</p>\n <p style=\"margin: 0;\">You are receiving this because you are subscribed to the ${r.plan} plan.</p>\n </div>\n</div>`;\n\nreturn [\n {\n json: {\n to: r.owner_email,\n subject: subject,\n html: htmlBody,\n tenant_id: r.tenant_id,\n company_name: r.company_name,\n month_label: monthLabel\n }\n }\n];"
},
"id": "wf008-node-build-email",
"name": "Build HTML Report Email",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
2000,
300
]
},
{
"parameters": {
"fromEmail": "user@example.com",
"toEmail": "={{ $json.to }}",
"subject": "={{ $json.subject }}",
"emailType": "html",
"html": "={{ $json.html }}"
},
"id": "wf008-node-send-email",
"name": "Send Monthly Report",
"type": "n8n-nodes-base.emailSend",
"typeVersion": 2.1,
"position": [
2250,
300
],
"credentials": {
"smtp": {
"name": "<your credential>"
}
}
},
{
"parameters": {
"operation": "create",
"tableId": "notifications",
"dataToSend": "defineBelow",
"fieldsUi": {
"fieldValues": [
{
"fieldName": "tenant_id",
"fieldValue": "={{ $json.tenant_id }}"
},
{
"fieldName": "type",
"fieldValue": "monthly_report"
},
{
"fieldName": "title",
"fieldValue": "={{ 'Monthly Report - ' + $json.month_label }}"
},
{
"fieldName": "message",
"fieldValue": "={{ 'Monthly performance report for ' + $json.month_label + ' sent to ' + $json.company_name }}"
},
{
"fieldName": "channel",
"fieldValue": "email"
},
{
"fieldName": "sent_at",
"fieldValue": "={{ new Date().toISOString() }}"
}
]
}
},
"id": "wf008-node-log-notification",
"name": "Log Report Notification",
"type": "n8n-nodes-base.supabase",
"typeVersion": 1,
"position": [
2500,
300
],
"credentials": {
"supabaseApi": {
"name": "<your credential>"
}
}
}
],
"connections": {
"1st of Month 9AM ET": {
"main": [
[
{
"node": "Get Active Tenants",
"type": "main",
"index": 0
}
]
]
},
"Get Active Tenants": {
"main": [
[
{
"node": "Loop Over Tenants",
"type": "main",
"index": 0
}
]
]
},
"Loop Over Tenants": {
"main": [
null,
[
{
"node": "Get Monthly Usage",
"type": "main",
"index": 0
}
]
]
},
"Get Monthly Usage": {
"main": [
[
{
"node": "Get Daily Trends",
"type": "main",
"index": 0
}
]
]
},
"Get Daily Trends": {
"main": [
[
{
"node": "Compute Insights",
"type": "main",
"index": 0
}
]
]
},
"Compute Insights": {
"main": [
[
{
"node": "Prepare Insight Records",
"type": "main",
"index": 0
}
]
]
},
"Prepare Insight Records": {
"main": [
[
{
"node": "Insert AI Insights",
"type": "main",
"index": 0
}
]
]
},
"Insert AI Insights": {
"main": [
[
{
"node": "Build HTML Report Email",
"type": "main",
"index": 0
}
]
]
},
"Build HTML Report Email": {
"main": [
[
{
"node": "Send Monthly Report",
"type": "main",
"index": 0
}
]
]
},
"Send Monthly Report": {
"main": [
[
{
"node": "Log Report Notification",
"type": "main",
"index": 0
}
]
]
},
"Log Report Notification": {
"main": [
[
{
"node": "Loop Over Tenants",
"type": "main",
"index": 0
}
]
]
}
},
"settings": {
"executionOrder": "v1",
"saveManualExecutions": true,
"callerPolicy": "workflowsFromSameOwner",
"timezone": "America/New_York"
},
"meta": {
"templateCredsSetupCompleted": false
},
"active": false,
"tags": [
{
"name": "reporting"
},
{
"name": "monthly"
},
{
"name": "insights"
}
]
}
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.
smtpsupabaseApi
For the full experience including quality scoring and batch install features for each workflow upgrade to Pro
About this workflow
WF-008 Monthly Report. Uses supabase, httpRequest, emailSend. Scheduled trigger; 11 nodes.
Source: https://github.com/rafiulislam4246/voice-sdr-title-agent/blob/main/app/n8n-workflows/WF-008-monthly-report.json — original creator credit. Request a take-down →
Related workflows
Workflows that share integrations, category, or trigger type with this one. All free to copy and import.
WF-005 Overage Warning. Uses httpRequest, emailSend, supabase. Scheduled trigger; 11 nodes.
WF-004 Renewal Reminder. Uses httpRequest, emailSend, supabase. Scheduled trigger; 8 nodes.
Monitors brand mentions across Twitter/X, Reddit, and News APIs in real-time (or scheduled), fetches mentions in parallel, normalizes data, uses AI to analyze sentiment/urgency/topics, detects duplica
Tech Daily Digest. Uses rssFeedRead, emailSend, httpRequest. Scheduled trigger; 22 nodes.
Ai Assistant Agent Full Workflow. Uses supabase, httpRequest, emailSend. Webhook trigger; 18 nodes.