This workflow follows the Airtable → 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": "04 - Escalate (F5)",
"nodes": [
{
"parameters": {
"rule": {
"interval": [
{
"field": "cronExpression",
"expression": "0 18 * * *"
}
]
}
},
"id": "Schedule 18:00",
"name": "Schedule 18:00",
"type": "n8n-nodes-base.scheduleTrigger",
"typeVersion": 1.2,
"position": [
-400,
300
],
"notes": "Office tz comes from workflow Settings -> Timezone (Europe/Istanbul)."
},
{
"parameters": {
"resource": "record",
"operation": "search",
"base": {
"__rl": true,
"mode": "id",
"value": "={{ $env.AIRTABLE_BASE_ID }}"
},
"table": {
"__rl": true,
"mode": "name",
"value": "Employees"
},
"filterByFormula": "",
"options": {}
},
"id": "Fetch Employees",
"name": "Fetch Employees",
"type": "n8n-nodes-base.airtable",
"typeVersion": 2.1,
"position": [
-180,
300
],
"executeOnce": true,
"notes": "Attach Airtable credential after import."
},
{
"parameters": {
"resource": "record",
"operation": "search",
"base": {
"__rl": true,
"mode": "id",
"value": "={{ $env.AIRTABLE_BASE_ID }}"
},
"table": {
"__rl": true,
"mode": "name",
"value": "Onboarding_Tasks"
},
"filterByFormula": "{Status} = 'Pending'",
"options": {}
},
"id": "Fetch Pending Tasks",
"name": "Fetch Pending Tasks",
"type": "n8n-nodes-base.airtable",
"typeVersion": 2.1,
"position": [
40,
300
],
"executeOnce": true,
"notes": "Attach Airtable credential after import."
},
{
"parameters": {
"language": "javaScript",
"jsCode": "// ============================================================================\n// GENERATED by `npm run build:nodes` \u2014 DO NOT EDIT.\n// Wrapper: workflows/code-nodes/escalate.wrapper.js\n// Core inlined verbatim from: scripts/dates.js, scripts/escalate.js\n// Paste this whole file into the n8n Code node.\n// ============================================================================\n\n// n8n Code node \u2014 \"Select Overdue\" (F5) (THIN WRAPPER \u2014 do not put logic here)\n// Core (dates.js + escalate.js) inlined by `npm run build:nodes`. Paste escalate.built.js.\n//\n// Input: Pending Onboarding_Tasks. Ref: \"Fetch Employees\" (live name). Env: OFFICE_TIMEZONE.\n// Output: one item per OVERDUE task, carrying the fields the update and the summary need\n// (Escalation_Count already bumped, Escalated_On = today). No overdue tasks -> no items ->\n// the bump and the HR summary downstream simply do not run.\n\n// ---- inlined from scripts/dates.js ----\n// dates.js \u2014 due-date arithmetic for onboarding tasks.\n//\n// Pure and deterministic: operates only on the ISO string it is given, in UTC.\n// No `Date.now()`, no local timezone, no external date library. This is on purpose \u2014\n// timezone only matters when comparing against \"today\" (notification / escalation\n// logic), never in the offset arithmetic itself, so this module stays trivially\n// testable outside n8n.\n//\n// dueDateFromOffset(startDate, offset):\n// Day_Offset is CALENDAR days (HR edits it and thinks calendar \u2014 \"contract 3 days\n// before start\"), added to the start date. If the result lands on a weekend it is\n// nudged to a working day, in the direction that keeps the deadline safe:\n// offset > 0 (after start) -> shift FORWARD to Monday\n// offset < 0 (prep before start) -> shift BACKWARD to Friday, so a prep task\n// never slips onto or past the day it precedes\n// offset === 0 (the start day itself) -> returned as-is, never adjusted; per\n// SPEC F1 the start date does not move, only\n// task deadlines do\n// All I/O is ISO `YYYY-MM-DD`.\n\nconst ISO_RE = /^\\d{4}-\\d{2}-\\d{2}$/;\nconst MS_PER_DAY = 86_400_000;\n\n/** Parse a strict ISO date to a UTC timestamp, rejecting malformed or impossible dates. */\nfunction parseISO(isoDate) {\n if (typeof isoDate !== 'string' || !ISO_RE.test(isoDate)) {\n throw new Error(`Invalid ISO date: ${JSON.stringify(isoDate)} (expected \"YYYY-MM-DD\")`);\n }\n const [y, m, d] = isoDate.split('-').map(Number);\n const ts = Date.UTC(y, m - 1, d);\n const back = new Date(ts);\n // Reject values JS would silently roll over, e.g. 2026-02-31 -> 2026-03-03.\n if (back.getUTCFullYear() !== y || back.getUTCMonth() !== m - 1 || back.getUTCDate() !== d) {\n throw new Error(`Invalid calendar date: ${isoDate}`);\n }\n return ts;\n}\n\n/** Format a UTC timestamp back to ISO `YYYY-MM-DD`. */\nfunction toISO(ts) {\n const dt = new Date(ts);\n const y = dt.getUTCFullYear();\n const m = String(dt.getUTCMonth() + 1).padStart(2, '0');\n const d = String(dt.getUTCDate()).padStart(2, '0');\n return `${y}-${m}-${d}`;\n}\n\nfunction isWeekendTs(ts) {\n const day = new Date(ts).getUTCDay(); // 0 = Sun \u2026 6 = Sat\n return day === 0 || day === 6;\n}\n\n/** True if the ISO date falls on Saturday or Sunday. */\nfunction isWeekend(isoDate) {\n return isWeekendTs(parseISO(isoDate));\n}\n\n/**\n * Compute a task due date from the start date and a calendar-day offset, keeping the\n * result on a working day (see the sign rules in the file header).\n * @param {string} startDate - start date, ISO `YYYY-MM-DD`\n * @param {number} offset - integer calendar-day offset; negative = before the start date\n * @returns {string} the resulting due date, ISO `YYYY-MM-DD`\n */\nfunction dueDateFromOffset(startDate, offset) {\n if (!Number.isInteger(offset)) {\n throw new Error(`offset must be an integer, got: ${JSON.stringify(offset)}`);\n }\n let ts = parseISO(startDate) + offset * MS_PER_DAY;\n if (offset === 0) {\n return toISO(ts); // the start day itself \u2014 never weekend-adjusted\n }\n const step = offset > 0 ? MS_PER_DAY : -MS_PER_DAY; // forward for after-start, backward for prep\n while (isWeekendTs(ts)) {\n ts += step;\n }\n return toISO(ts);\n}\n\n// ---- inlined from scripts/escalate.js ----\n// escalate.js \u2014 pure logic for F5 (daily overdue escalation to HR).\n//\n// No network, no Date.now(). `today` is injected as an ISO date (office-local, computed in\n// the wrapper) so the day boundary is testable.\n\n\nconst DAY_MS = 86_400_000;\n\n/**\n * Overdue tasks to escalate: Pending, due STRICTLY before today (a task due today is not\n * overdue), and not already escalated today (idempotent across re-runs via Escalated_On).\n */\nfunction selectOverdue(tasks, today) {\n return tasks.filter(\n (t) => t.status === 'Pending' && String(t.due_date) < today && String(t.escalated_on || '') !== today,\n );\n}\n\n/** Whole calendar days a task is overdue (today \u2212 due_date). */\nfunction daysOverdue(dueDate, today) {\n return Math.round((parseISO(today) - parseISO(dueDate)) / DAY_MS);\n}\n\n/**\n * One update per overdue task: bump Escalation_Count and stamp Escalated_On = today.\n * Escalated_On is what stops a second run the same day from escalating again.\n */\nfunction escalationUpdates(tasks, today) {\n return tasks.map((t) => ({\n id: t.id,\n Escalation_Count: Number(t.escalation_count || 0) + 1,\n Escalated_On: today,\n }));\n}\n\n/**\n * Build the single grouped HR summary for the overdue tasks, grouped by new hire.\n * @returns {{ text: string, count: number }}\n */\nfunction buildEscalationSummary(tasks, today) {\n const byEmployee = new Map();\n for (const t of tasks) {\n if (!byEmployee.has(t.employee_id)) byEmployee.set(t.employee_id, []);\n byEmployee.get(t.employee_id).push(t);\n }\n\n const lines = [`Overdue onboarding tasks (${tasks.length}):`];\n for (const [, empTasks] of byEmployee) {\n lines.push('', empTasks[0].employee_name || '(unknown employee)');\n for (const t of empTasks) {\n const n = daysOverdue(t.due_date, today);\n lines.push(` ${t.title} \u2014 ${t.assignee_role}, due ${t.due_date} (${n}d overdue)`);\n }\n }\n return { text: lines.join('\\n'), count: tasks.length };\n}\n\nconst flat = (v) => (Array.isArray(v) ? v[0] : v);\nconst tz = $env.OFFICE_TIMEZONE || 'UTC';\nconst today = new Date().toLocaleDateString('en-CA', { timeZone: tz });\n\nconst empById = {};\nfor (const i of $('Fetch Employees').all()) empById[i.json.id] = { name: i.json.Full_Name };\n\nconst tasks = items.map((i) => {\n const f = i.json;\n const employee_id = flat(f.Employee);\n return {\n id: f.id, title: f.Title, due_date: f.Due_Date, status: f.Status,\n escalated_on: f.Escalated_On || '', escalation_count: f.Escalation_Count,\n assignee_role: f.Assignee_Role, employee_id, employee_name: (empById[employee_id] || {}).name,\n };\n});\n\nconst overdue = selectOverdue(tasks, today);\nconst bumpById = Object.fromEntries(escalationUpdates(overdue, today).map((u) => [u.id, u]));\nreturn overdue.map((t) => ({ json: { ...t, Escalation_Count: bumpById[t.id].Escalation_Count, Escalated_On: today, _today: today } }));\n"
},
"id": "Select Overdue",
"name": "Select Overdue",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
260,
300
]
},
{
"parameters": {
"method": "PATCH",
"url": "=https://api.airtable.com/v0/{{ $env.AIRTABLE_BASE_ID }}/Onboarding_Tasks/{{ $json.id }}",
"authentication": "predefinedCredentialType",
"nodeCredentialType": "airtableTokenApi",
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={{ JSON.stringify({ fields: { Escalation_Count: $json.Escalation_Count, Escalated_On: $json.Escalated_On } }) }}",
"options": {}
},
"id": "Bump Escalation",
"name": "Bump Escalation",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
480,
200
],
"notes": "Airtable update via HTTP PATCH; attach the Airtable PAT credential (Predefined Credential Type)."
},
{
"parameters": {
"language": "javaScript",
"jsCode": "// ============================================================================\n// GENERATED by `npm run build:nodes` \u2014 DO NOT EDIT.\n// Wrapper: workflows/code-nodes/escalateSummary.wrapper.js\n// Core inlined verbatim from: scripts/dates.js, scripts/escalate.js\n// Paste this whole file into the n8n Code node.\n// ============================================================================\n\n// n8n Code node \u2014 \"HR Summary\" (F5) (THIN WRAPPER \u2014 do not put logic here)\n// Core (dates.js + escalate.js) inlined by `npm run build:nodes`. Paste escalateSummary.built.js.\n//\n// Input: the overdue tasks from \"Select Overdue\". Output: a single { text, count } \u2014 one\n// grouped HR message, never one per task.\n\n// ---- inlined from scripts/dates.js ----\n// dates.js \u2014 due-date arithmetic for onboarding tasks.\n//\n// Pure and deterministic: operates only on the ISO string it is given, in UTC.\n// No `Date.now()`, no local timezone, no external date library. This is on purpose \u2014\n// timezone only matters when comparing against \"today\" (notification / escalation\n// logic), never in the offset arithmetic itself, so this module stays trivially\n// testable outside n8n.\n//\n// dueDateFromOffset(startDate, offset):\n// Day_Offset is CALENDAR days (HR edits it and thinks calendar \u2014 \"contract 3 days\n// before start\"), added to the start date. If the result lands on a weekend it is\n// nudged to a working day, in the direction that keeps the deadline safe:\n// offset > 0 (after start) -> shift FORWARD to Monday\n// offset < 0 (prep before start) -> shift BACKWARD to Friday, so a prep task\n// never slips onto or past the day it precedes\n// offset === 0 (the start day itself) -> returned as-is, never adjusted; per\n// SPEC F1 the start date does not move, only\n// task deadlines do\n// All I/O is ISO `YYYY-MM-DD`.\n\nconst ISO_RE = /^\\d{4}-\\d{2}-\\d{2}$/;\nconst MS_PER_DAY = 86_400_000;\n\n/** Parse a strict ISO date to a UTC timestamp, rejecting malformed or impossible dates. */\nfunction parseISO(isoDate) {\n if (typeof isoDate !== 'string' || !ISO_RE.test(isoDate)) {\n throw new Error(`Invalid ISO date: ${JSON.stringify(isoDate)} (expected \"YYYY-MM-DD\")`);\n }\n const [y, m, d] = isoDate.split('-').map(Number);\n const ts = Date.UTC(y, m - 1, d);\n const back = new Date(ts);\n // Reject values JS would silently roll over, e.g. 2026-02-31 -> 2026-03-03.\n if (back.getUTCFullYear() !== y || back.getUTCMonth() !== m - 1 || back.getUTCDate() !== d) {\n throw new Error(`Invalid calendar date: ${isoDate}`);\n }\n return ts;\n}\n\n/** Format a UTC timestamp back to ISO `YYYY-MM-DD`. */\nfunction toISO(ts) {\n const dt = new Date(ts);\n const y = dt.getUTCFullYear();\n const m = String(dt.getUTCMonth() + 1).padStart(2, '0');\n const d = String(dt.getUTCDate()).padStart(2, '0');\n return `${y}-${m}-${d}`;\n}\n\nfunction isWeekendTs(ts) {\n const day = new Date(ts).getUTCDay(); // 0 = Sun \u2026 6 = Sat\n return day === 0 || day === 6;\n}\n\n/** True if the ISO date falls on Saturday or Sunday. */\nfunction isWeekend(isoDate) {\n return isWeekendTs(parseISO(isoDate));\n}\n\n/**\n * Compute a task due date from the start date and a calendar-day offset, keeping the\n * result on a working day (see the sign rules in the file header).\n * @param {string} startDate - start date, ISO `YYYY-MM-DD`\n * @param {number} offset - integer calendar-day offset; negative = before the start date\n * @returns {string} the resulting due date, ISO `YYYY-MM-DD`\n */\nfunction dueDateFromOffset(startDate, offset) {\n if (!Number.isInteger(offset)) {\n throw new Error(`offset must be an integer, got: ${JSON.stringify(offset)}`);\n }\n let ts = parseISO(startDate) + offset * MS_PER_DAY;\n if (offset === 0) {\n return toISO(ts); // the start day itself \u2014 never weekend-adjusted\n }\n const step = offset > 0 ? MS_PER_DAY : -MS_PER_DAY; // forward for after-start, backward for prep\n while (isWeekendTs(ts)) {\n ts += step;\n }\n return toISO(ts);\n}\n\n// ---- inlined from scripts/escalate.js ----\n// escalate.js \u2014 pure logic for F5 (daily overdue escalation to HR).\n//\n// No network, no Date.now(). `today` is injected as an ISO date (office-local, computed in\n// the wrapper) so the day boundary is testable.\n\n\nconst DAY_MS = 86_400_000;\n\n/**\n * Overdue tasks to escalate: Pending, due STRICTLY before today (a task due today is not\n * overdue), and not already escalated today (idempotent across re-runs via Escalated_On).\n */\nfunction selectOverdue(tasks, today) {\n return tasks.filter(\n (t) => t.status === 'Pending' && String(t.due_date) < today && String(t.escalated_on || '') !== today,\n );\n}\n\n/** Whole calendar days a task is overdue (today \u2212 due_date). */\nfunction daysOverdue(dueDate, today) {\n return Math.round((parseISO(today) - parseISO(dueDate)) / DAY_MS);\n}\n\n/**\n * One update per overdue task: bump Escalation_Count and stamp Escalated_On = today.\n * Escalated_On is what stops a second run the same day from escalating again.\n */\nfunction escalationUpdates(tasks, today) {\n return tasks.map((t) => ({\n id: t.id,\n Escalation_Count: Number(t.escalation_count || 0) + 1,\n Escalated_On: today,\n }));\n}\n\n/**\n * Build the single grouped HR summary for the overdue tasks, grouped by new hire.\n * @returns {{ text: string, count: number }}\n */\nfunction buildEscalationSummary(tasks, today) {\n const byEmployee = new Map();\n for (const t of tasks) {\n if (!byEmployee.has(t.employee_id)) byEmployee.set(t.employee_id, []);\n byEmployee.get(t.employee_id).push(t);\n }\n\n const lines = [`Overdue onboarding tasks (${tasks.length}):`];\n for (const [, empTasks] of byEmployee) {\n lines.push('', empTasks[0].employee_name || '(unknown employee)');\n for (const t of empTasks) {\n const n = daysOverdue(t.due_date, today);\n lines.push(` ${t.title} \u2014 ${t.assignee_role}, due ${t.due_date} (${n}d overdue)`);\n }\n }\n return { text: lines.join('\\n'), count: tasks.length };\n}\n\nif (items.length === 0) return [];\nconst today = items[0].json._today;\nconst tasks = items.map((i) => {\n const f = i.json;\n return { title: f.title, due_date: f.due_date, assignee_role: f.assignee_role, employee_id: f.employee_id, employee_name: f.employee_name };\n});\nconst { text, count } = buildEscalationSummary(tasks, today);\nreturn [{ json: { text, count } }];\n"
},
"id": "HR Summary",
"name": "HR Summary",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
480,
420
]
},
{
"parameters": {
"method": "POST",
"url": "=https://api.telegram.org/bot{{ $env.TELEGRAM_BOT_TOKEN }}/sendMessage",
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={{ JSON.stringify({ chat_id: $env.TELEGRAM_HR_CHAT_ID, text: $json.text }) }}",
"options": {}
},
"id": "Send HR Summary",
"name": "Send HR Summary",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
700,
420
]
}
],
"connections": {
"Schedule 18:00": {
"main": [
[
{
"node": "Fetch Employees",
"type": "main",
"index": 0
}
]
]
},
"Fetch Employees": {
"main": [
[
{
"node": "Fetch Pending Tasks",
"type": "main",
"index": 0
}
]
]
},
"Fetch Pending Tasks": {
"main": [
[
{
"node": "Select Overdue",
"type": "main",
"index": 0
}
]
]
},
"Select Overdue": {
"main": [
[
{
"node": "Bump Escalation",
"type": "main",
"index": 0
},
{
"node": "HR Summary",
"type": "main",
"index": 0
}
]
]
},
"HR Summary": {
"main": [
[
{
"node": "Send HR Summary",
"type": "main",
"index": 0
}
]
]
}
},
"active": false,
"settings": {
"executionOrder": "v1",
"timezone": "Europe/Istanbul"
},
"meta": {
"note": "Day boundary + once-per-day guard (Escalated_On) handled in code. No overdue tasks -> no bump, no message. Set Settings -> Error Workflow to notify HR. TELEGRAM_BOT_TOKEN / TELEGRAM_HR_CHAT_ID + Airtable credential required."
}
}
For the full experience including quality scoring and batch install features for each workflow upgrade to Pro
About this workflow
04 - Escalate (F5). Uses airtable, httpRequest. Scheduled trigger; 7 nodes.
Source: https://github.com/ibragim-0202/employee-onboarding-automation/blob/main/workflows/04-escalate.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.
I prepared a detailed guide that showed the whole process of integrating the Binance API and storing data in Airtable to manage funding statements associated with tokens in a wallet.
Stop wasting hours on manual dialing and listening to ringtones. This workflow transforms your Airtable into a high-velocity AI Call Center using Vapi AI**.
Reel-Analysis-Of-Favourite-Content-Creator. Uses httpRequest, airtable. Scheduled trigger; 26 nodes.
Link-By-Reel-Analysis. Uses httpRequest, airtable. Scheduled trigger; 24 nodes.
> Transform your content strategy with automated competitor intelligence