This workflow corresponds to n8n.io template #14306 — we link there as the canonical source.
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 →
{
"nodes": [
{
"id": "fc46a11c-cb88-470d-ac28-314a4f911676",
"name": "Send an Email",
"type": "n8n-nodes-base.emailSend",
"position": [
2704,
176
],
"parameters": {
"html": "={{ $json.html }}",
"options": {
"appendAttribution": false,
"allowUnauthorizedCerts": true
},
"subject": "={{ $json.subject }}",
"toEmail": "info@example.com",
"fromEmail": "info@example.com"
},
"credentials": {
"smtp": {
"name": "<your credential>"
}
},
"typeVersion": 2.1
},
{
"id": "51e18cb1-20e1-486a-9ea5-a9c65bb72a27",
"name": "Every Day at 9:00",
"type": "n8n-nodes-base.scheduleTrigger",
"position": [
736,
256
],
"parameters": {
"rule": {
"interval": [
{
"field": "cronExpression",
"expression": "0 9 * * 1-5"
}
]
}
},
"typeVersion": 1.2
},
{
"id": "f228e077-eae6-4442-abd5-46d713a980ff",
"name": "GET Projects",
"type": "n8n-nodes-base.httpRequest",
"position": [
960,
256
],
"parameters": {
"url": "https://kimai/api/projects",
"options": {},
"sendQuery": true,
"authentication": "genericCredentialType",
"genericAuthType": "httpBearerAuth",
"queryParameters": {
"parameters": [
{
"name": "visible",
"value": "1"
}
]
}
},
"credentials": {
"httpBearerAuth": {
"name": "<your credential>"
}
},
"typeVersion": 4.2
},
{
"id": "055cc8ac-f30a-4f69-a236-edd0425b58cc",
"name": "Get only Bilable",
"type": "n8n-nodes-base.code",
"position": [
1184,
256
],
"parameters": {
"jsCode": "const results = [];\n\nfor (const item of $input.all()) {\n if (item.json.billable === true) {\n results.push({ json: item.json });\n }\n}\n\nreturn results;"
},
"typeVersion": 2
},
{
"id": "0e709eaf-f296-4e60-8fa5-20739dd4a9e8",
"name": "GET Projects Details",
"type": "n8n-nodes-base.httpRequest",
"position": [
1552,
160
],
"parameters": {
"url": "=https://kimai/api/projects/{{$json.id}}",
"options": {},
"authentication": "genericCredentialType",
"genericAuthType": "httpBearerAuth"
},
"credentials": {
"httpBearerAuth": {
"name": "<your credential>"
}
},
"typeVersion": 4.2
},
{
"id": "0a14fabc-e525-4c78-8291-b1f9dad92492",
"name": "GET Timesheet Records",
"type": "n8n-nodes-base.httpRequest",
"position": [
1408,
352
],
"parameters": {
"url": "https://kimai/api/timesheets",
"options": {},
"sendQuery": true,
"authentication": "genericCredentialType",
"genericAuthType": "httpBearerAuth",
"queryParameters": {
"parameters": [
{
"name": "user",
"value": "all"
},
{
"name": "project",
"value": "={{ $json.id }}"
},
{
"name": "size",
"value": "1+1234567890"
}
]
}
},
"credentials": {
"httpBearerAuth": {
"name": "<your credential>"
}
},
"typeVersion": 4.2
},
{
"id": "3453b9a8-0e24-422b-972f-ad490bdc8fcf",
"name": "Calculate Budget Uses",
"type": "n8n-nodes-base.code",
"position": [
1632,
352
],
"parameters": {
"jsCode": "const totali = {};\n\nfor (const item of $input.all()) {\n const projectId = item.json.project?.id ?? item.json.project ?? null;\n const duration = Number(item.json.duration ?? 0);\n\n if (!projectId) continue;\n\n if (!totali[projectId]) {\n totali[projectId] = 0;\n }\n\n totali[projectId] += duration;\n}\n\nconst results = Object.entries(totali)\n .map(([id, totalSeconds]) => ({\n json: {\n id: Number(id),\n total_seconds: totalSeconds,\n total_ore: Math.round((totalSeconds / 3600) * 100) / 100\n }\n }))\n .sort((a, b) => a.json.id - b.json.id);\n\nreturn results;"
},
"typeVersion": 2
},
{
"id": "381fbd24-3e48-4d3f-8b92-ae73df6d8456",
"name": "Combine Data",
"type": "n8n-nodes-base.merge",
"position": [
1856,
256
],
"parameters": {
"mode": "combine",
"options": {},
"joinMode": "enrichInput1",
"fieldsToMatchString": "id"
},
"typeVersion": 3.2
},
{
"id": "1f697c3b-9d42-4f15-ac8e-98c5b0490a38",
"name": "Calculate expiration",
"type": "n8n-nodes-base.code",
"position": [
2080,
256
],
"parameters": {
"jsCode": "const daysThreshold = 10;\n\nconst now = new Date();\nconst today = new Date(now.getFullYear(), now.getMonth(), now.getDate());\n\nconst msPerDay = 1000 * 60 * 60 * 24;\nconst projects = [];\n\nfunction formatIsoToItalian(isoDate) {\n if (!isoDate) return null;\n\n const parts = String(isoDate).split(\"-\");\n if (parts.length !== 3) return null;\n\n return `${parts[2]}/${parts[1]}/${parts[0]}`;\n}\n\nfunction getDaysDiff(date) {\n if (!date) return null;\n return Math.round((date.getTime() - today.getTime()) / msPerDay);\n}\n\nfunction getUrgency(days) {\n if (days === null || days === undefined) return \"none\";\n if (days < 0) return \"expired\";\n if (days <= 3) return \"high\";\n if (days <= 7) return \"medium\";\n return \"low\";\n}\n\nfunction getStatus(days) {\n if (days === null || days === undefined) return \"not_present\";\n return days < 0 ? \"expired\" : \"expiring\";\n}\n\nfunction getBudgetInfo(timeBudget, totalSeconds) {\n if (!timeBudget || timeBudget <= 0) return null;\n\n const percentage = Math.round((totalSeconds / timeBudget) * 100);\n const usedHours = Math.round((totalSeconds / 3600) * 100) / 100;\n const budgetHours = Math.round((timeBudget / 3600) * 100) / 100;\n const remainingHours = Math.round(((timeBudget - totalSeconds) / 3600) * 100) / 100;\n\n return {\n percentage,\n usedHours,\n budgetHours,\n remainingHours,\n exceeded: percentage >= 100,\n shouldReport: percentage >= 80\n };\n}\n\nfor (const item of $input.all()) {\n const id = item.json.id ?? null;\n const name = String(item.json.name ?? \"\");\n const customer = String(item.json.parentTitle ?? \"\");\n\n const endRaw = item.json.end ?? null;\n\n let projectDeadline = \"not present\";\n let projectDeadlineIso = null;\n let projectDays = null;\n let projectUrgency = \"none\";\n let projectStatus = \"not_present\";\n\n if (endRaw) {\n const endDate = new Date(`${endRaw}T00:00:00`);\n if (!isNaN(endDate.getTime())) {\n projectDeadline = formatIsoToItalian(endRaw) || endRaw;\n projectDeadlineIso = endRaw;\n projectDays = getDaysDiff(endDate);\n projectUrgency = getUrgency(projectDays);\n projectStatus = getStatus(projectDays);\n }\n }\n\n const timeBudget = Number(item.json.timeBudget ?? 0);\n const totalSeconds = Number(item.json.total_seconds ?? 0);\n const budget = getBudgetInfo(timeBudget, totalSeconds);\n\n const projectShouldReport =\n projectDays !== null && projectDays >= 0 && projectDays <= daysThreshold;\n\n const budgetShouldReport =\n budget !== null && budget.shouldReport;\n\n const shouldReport =\n projectShouldReport ||\n budgetShouldReport;\n\n if (!shouldReport) {\n continue;\n }\n\n projects.push({\n id,\n name,\n customer,\n\n project_deadline: projectDeadline,\n project_deadline_iso: projectDeadlineIso,\n project_days: projectDays,\n project_urgency: projectUrgency,\n project_status: projectStatus,\n\n budget_set: timeBudget > 0,\n budget_total_hours: budget ? budget.budgetHours : null,\n budget_used_hours: budget ? budget.usedHours : null,\n budget_remaining_hours: budget ? budget.remainingHours : null,\n budget_percentage: budget ? budget.percentage : null,\n budget_exceeded: budget ? budget.exceeded : false,\n budget_should_report: budget ? budget.shouldReport : false\n });\n}\n\nprojects.sort((a, b) => {\n const aVal = a.project_days ?? 999999;\n const bVal = b.project_days ?? 999999;\n return aVal - bVal;\n});\n\nreturn [\n {\n json: {\n daysThreshold,\n count: projects.length,\n projects\n }\n }\n];"
},
"typeVersion": 2
},
{
"id": "753c9210-efd8-43bf-993c-6f50799b0dfb",
"name": "Need Email?",
"type": "n8n-nodes-base.if",
"position": [
2304,
256
],
"parameters": {
"options": {},
"conditions": {
"options": {
"version": 1,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "condition-count",
"operator": {
"type": "number",
"operation": "gt"
},
"leftValue": "={{ $json.count }}",
"rightValue": 0
}
]
}
},
"typeVersion": 2
},
{
"id": "a5294383-503e-4c15-a5e7-51e6e3ec0ed8",
"name": "Build Email HTML - Report",
"type": "n8n-nodes-base.code",
"position": [
2528,
176
],
"parameters": {
"jsCode": "const projects = $json.projects || [];\nconst todayDate = $now.setLocale('en').toFormat('dd LLL yyyy');\nconst count = Number($json.count || 0);\nconst daysThreshold = Number($json.daysThreshold || 10);\n\nconst urgencyConfig = {\n expired: { color: '#ef4444', background: '#fef2f2', icon: '\ud83d\udea8', label: 'EXPIRED', action: 'TAKE ACTION' },\n high: { color: '#f97316', background: '#fff7ed', icon: '\ud83d\udd25', label: 'URGENT', action: 'DUE SOON' },\n medium: { color: '#d97706', background: '#fffbeb', icon: '\u23f3', label: 'WARNING', action: 'TO MONITOR' },\n low: { color: '#10b981', background: '#ecfdf5', icon: '\u2705', label: 'OK', action: 'ON TRACK' },\n none: { color: '#6366f1', background: '#f5f3ff', icon: '\ud83d\udd0d', label: 'TO CHECK', action: 'MISSING DATA' }\n};\n\nconst escapeHtml = (v) =>\n String(v ?? '')\n .replace(/&/g, '&')\n .replace(/</g, '<')\n .replace(/>/g, '>');\n\nconst buildStatusBlock = (urgency, date, label) => {\n const cfg = urgencyConfig[urgency] || urgencyConfig.none;\n const isMissing = !date || date === 'not present';\n\n return `\n <div style=\"background: ${cfg.background}; border-radius: 16px; padding: 14px; border: 1px solid ${cfg.color}${isMissing ? '60' : '20'}; min-height: 80px;\">\n <div style=\"display: flex; align-items: center; margin-bottom: 6px;\">\n <span style=\"font-size: 14px; margin-right: 6px;\">${cfg.icon}</span>\n <span style=\"font-size: 10px; font-weight: 800; color: ${cfg.color}; text-transform: uppercase; letter-spacing: 1px;\">${cfg.label}</span>\n </div>\n <div style=\"font-size: 15px; font-weight: 800; color: ${isMissing ? cfg.color : '#1e293b'}; margin-bottom: 2px;\">\n ${escapeHtml(isMissing ? 'MISSING DATA' : date)}\n </div>\n <div style=\"font-size: 11px; color: #64748b; font-weight: 500;\">${escapeHtml(label)}</div>\n </div>\n `;\n};\n\nconst buildBudgetBlock = (p) => {\n if (!p.budget_set) {\n return `\n <div style=\"background: #f5f3ff; border-radius: 16px; padding: 16px; border: 1px dashed #6366f1; text-align: center;\">\n <div style=\"font-size: 14px; margin-bottom: 4px;\">\ud83d\udd0d</div>\n <div style=\"font-size: 11px; font-weight: 800; color: #6366f1; text-transform: uppercase;\">Budget not configured</div>\n <div style=\"font-size: 10px; color: #64748b; margin-top: 2px;\">Check whether this project should be billable</div>\n </div>`;\n }\n\n const percent = Math.min(Number(p.budget_percentage ?? 0), 100);\n const isOver = p.budget_exceeded;\n const color = isOver ? '#ef4444' : (percent > 85 ? '#f97316' : '#3b82f6');\n\n return `\n <div style=\"background: #ffffff; border-radius: 16px; padding: 16px; border: 1px solid #e2e8f0;\">\n <table width=\"100%\" cellpadding=\"0\" cellspacing=\"0\">\n <tr>\n <td style=\"font-size: 12px; font-weight: 700; color: #1e293b;\">Budget Usage</td>\n <td align=\"right\" style=\"font-size: 12px; font-weight: 800; color: ${color};\">${p.budget_percentage}%</td>\n </tr>\n </table>\n <div style=\"width: 100%; height: 8px; background: #f1f5f9; border-radius: 10px; margin: 10px 0; overflow: hidden;\">\n <div style=\"width: ${percent}%; height: 100%; background: ${color}; border-radius: 10px;\"></div>\n </div>\n <table width=\"100%\" cellpadding=\"0\" cellspacing=\"0\">\n <tr>\n <td style=\"font-size: 11px; color: #64748b;\"><strong>${p.budget_used_hours}h</strong> logged</td>\n <td align=\"right\" style=\"font-size: 11px; color: #64748b;\">Total: <strong>${p.budget_total_hours}h</strong></td>\n </tr>\n </table>\n </div>`;\n};\n\nconst rowsHTML = projects.map((p) => {\n const projectLabel =\n p.project_status === 'not_present'\n ? 'Add the PO deadline'\n : (p.project_days < 0\n ? `Expired ${Math.abs(p.project_days)} days ago`\n : `In ${p.project_days} days`);\n\n return `\n <div style=\"margin-bottom: 30px; background: #ffffff; border-radius: 24px; box-shadow: 0 10px 25px rgba(0,0,0,0.05); border: 1px solid #e2e8f0; overflow: hidden;\">\n <div style=\"padding: 20px; background: linear-gradient(to right, #f8fafc, #ffffff); border-bottom: 1px solid #f1f5f9;\">\n <div style=\"font-size: 10px; font-weight: 800; color: #3b82f6; text-transform: uppercase; letter-spacing: 1.5px; margin-bottom: 4px;\">${escapeHtml(p.customer)}</div>\n <div style=\"font-size: 20px; font-weight: 900; color: #0f172a; letter-spacing: -0.5px;\">${escapeHtml(p.name)}</div>\n </div>\n \n <div style=\"padding: 20px;\">\n <div style=\"font-size: 10px; font-weight: 700; color: #94a3b8; text-transform: uppercase; margin-bottom: 8px;\">Order Deadline (PO)</div>\n ${buildStatusBlock(p.project_urgency, p.project_deadline, projectLabel)}\n\n <div style=\"height: 16px;\"></div>\n\n <div style=\"font-size: 10px; font-weight: 700; color: #94a3b8; text-transform: uppercase; margin-bottom: 8px;\">Hours Monitoring</div>\n ${buildBudgetBlock(p)}\n </div>\n </div>`;\n}).join('');\n\nconst html = `<!DOCTYPE html>\n<html lang=\"en\">\n<body style=\"margin:0; padding:0; background-color:#f8fafc; font-family:'Inter', -apple-system, system-ui, sans-serif;\">\n <table width=\"100%\" cellpadding=\"0\" cellspacing=\"0\" border=\"0\" style=\"background-color: #f8fafc; padding: 40px 10px;\">\n <tr>\n <td align=\"center\">\n <table width=\"600\" cellpadding=\"0\" cellspacing=\"0\" border=\"0\" style=\"width: 100%; max-width: 600px;\">\n <tr>\n <td style=\"padding-bottom: 30px; text-align: center;\">\n <div style=\"font-size: 12px; font-weight: 800; color: #3b82f6; text-transform: uppercase; letter-spacing: 2px; margin-bottom: 8px;\">Timesheet Analysis</div>\n <h1 style=\"font-size: 36px; font-weight: 900; color: #0f172a; margin: 0; letter-spacing: -1.5px;\">Deadline Report</h1>\n <p style=\"font-size: 16px; color: #64748b; margin-top: 10px;\">Detected <strong>${count} projects</strong> to manage or verify.</p>\n </td>\n </tr>\n\n <tr>\n <td>${rowsHTML}</td>\n </tr>\n\n <tr>\n <td style=\"padding-top: 20px; text-align: center;\">\n <a href=\"https://kimai.com\" style=\"display: inline-block; background: #0f172a; color: #ffffff; padding: 16px 40px; border-radius: 18px; text-decoration: none; font-weight: 700; font-size: 15px; box-shadow: 0 10px 20px rgba(15,23,42,0.1);\">Open Timesheet →</a>\n <div style=\"margin-top: 40px; border-top: 1px solid #e2e8f0; padding-top: 20px; font-size: 12px; color: #94a3b8; letter-spacing: 0.5px;\">\n Automatically generated on ${todayDate}\n </div>\n </td>\n </tr>\n </table>\n </td>\n </tr>\n </table>\n</body>\n</html>`;\n\nreturn [\n {\n json: {\n html,\n subject: `[Timesheet] - Projects Deadline Report`\n }\n }\n];"
},
"typeVersion": 2
},
{
"id": "d9e85095-fd10-4ad2-aa0e-15d2a44163d5",
"name": "Sticky Note",
"type": "n8n-nodes-base.stickyNote",
"position": [
2016,
544
],
"parameters": {
"color": 3,
"width": 224,
"height": 80,
"content": "To Customize the Range of Day to check customize here."
},
"typeVersion": 1
},
{
"id": "e0c4fc70-0a02-47e4-985a-8f30eca5690c",
"name": "Sticky Note1",
"type": "n8n-nodes-base.stickyNote",
"position": [
640,
32
],
"parameters": {
"color": 7,
"width": 256,
"height": 608,
"content": "## 1. Scheduled Start"
},
"typeVersion": 1
},
{
"id": "b7ef4e00-7322-4614-85aa-2d99ef0f5656",
"name": "Sticky Note2",
"type": "n8n-nodes-base.stickyNote",
"position": [
912,
32
],
"parameters": {
"color": 7,
"width": 1072,
"height": 608,
"content": "## 2. Get Information from Kimai"
},
"typeVersion": 1
},
{
"id": "e70f095a-c231-4a93-b813-ffd7d1acfb46",
"name": "Sticky Note3",
"type": "n8n-nodes-base.stickyNote",
"position": [
2000,
32
],
"parameters": {
"color": 7,
"width": 256,
"height": 608,
"content": "## 3. Check Expiration"
},
"typeVersion": 1
},
{
"id": "59990bba-f5c5-4b4a-8fb0-f845d04e3d8b",
"name": "Sticky Note4",
"type": "n8n-nodes-base.stickyNote",
"position": [
2272,
32
],
"parameters": {
"color": 7,
"width": 624,
"height": 608,
"content": "## 4. Build & Send Email"
},
"typeVersion": 1
},
{
"id": "6aae9924-997a-4887-a127-1a8a35a7fd15",
"name": "Sticky Note5",
"type": "n8n-nodes-base.stickyNote",
"position": [
-144,
32
],
"parameters": {
"width": 768,
"height": 608,
"content": "# \ud83d\udcc5 Kimai \u2014 Deadline & Budget Monitor\n\n## Monitors billable Kimai projects daily and sends an HTML alert email when a deadline is within 10 days or the hour budget exceeds 80%.\n\nRuns every weekday at **9 AM**.\nFetches all billable projects and checks:\n- End date within **10 days**\n- Budget consumption **\u2265 80%**\n\nNo alerts needed? No email sent.\n\n## \u2699\ufe0f Customize\n\n| What | Where |\n|---|---|\n| Days threshold | `Calculate expiration` \u2192 line 1 |\n| Budget % alert | `Calculate expiration` \u2192 `getBudgetInfo()` |\n| Kimai URL | All 3 HTTP Request nodes |\n| Sender / Recipient | `Send an Email` node |\n"
},
"typeVersion": 1
}
],
"connections": {
"Need Email?": {
"main": [
[
{
"node": "Build Email HTML - Report",
"type": "main",
"index": 0
}
]
]
},
"Combine Data": {
"main": [
[
{
"node": "Calculate expiration",
"type": "main",
"index": 0
}
]
]
},
"GET Projects": {
"main": [
[
{
"node": "Get only Bilable",
"type": "main",
"index": 0
}
]
]
},
"Get only Bilable": {
"main": [
[
{
"node": "GET Projects Details",
"type": "main",
"index": 0
},
{
"node": "GET Timesheet Records",
"type": "main",
"index": 0
}
]
]
},
"Every Day at 9:00": {
"main": [
[
{
"node": "GET Projects",
"type": "main",
"index": 0
}
]
]
},
"Calculate expiration": {
"main": [
[
{
"node": "Need Email?",
"type": "main",
"index": 0
}
]
]
},
"GET Projects Details": {
"main": [
[
{
"node": "Combine Data",
"type": "main",
"index": 0
}
]
]
},
"Calculate Budget Uses": {
"main": [
[
{
"node": "Combine Data",
"type": "main",
"index": 1
}
]
]
},
"GET Timesheet Records": {
"main": [
[
{
"node": "Calculate Budget Uses",
"type": "main",
"index": 0
}
]
]
},
"Build Email HTML - Report": {
"main": [
[
{
"node": "Send an Email",
"type": "main",
"index": 0
}
]
]
}
}
}
Credentials you'll need
Each integration node will prompt for credentials when you import. We strip credential IDs before publishing — you'll add your own.
httpBearerAuthsmtp
For the full experience including quality scoring and batch install features for each workflow upgrade to Pro
About this workflow
Automatically monitor billable Kimai projects every weekday morning and receive a formatted HTML email when a project deadline is approaching or its hour budget is running low. If nothing requires attention, no email is sent keeping your inbox clean and focused.
Source: https://n8n.io/workflows/14306/ — 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.
Easily monitor your website uptime and receive instant email alerts when it becomes unreachable — using this no-code template powered by n8n, a free and flexible workflow automation tool.
Proactively alert to service endpoint changes and pod/container issues (Pending, Not Ready, Restart spikes) using Prometheus metrics, formatted and sent to Slack.
Tired of being let down by the Google Drive Trigger? Rather not exhaust system resources by polling every minute? Then this workflow is for you!
Triggers at a regular interval or via a webhook request. Solves AWS WAF challenge then makes a request to fetch the product page. Extracts product data from the retrieved HTML page. Compares the curre
Stay ahead of credential expirations by automatically detecting Entra ID application client secrets and certificates that are about to expire, and sending a neatly formatted email report.