AutomationFlowsData & Sheets › Escalate (f5)

Escalate (f5)

04 - Escalate (F5). Uses airtable, httpRequest. Scheduled trigger; 7 nodes.

Cron / scheduled trigger★★★★☆ complexity7 nodesAirtableHTTP Request
Data & Sheets Trigger: Cron / scheduled Nodes: 7 Complexity: ★★★★☆ Added:

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 →

Download .json
{
  "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."
  }
}
Pro

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 →

More Data & Sheets workflows → · Browse all categories →

Related workflows

Workflows that share integrations, category, or trigger type with this one. All free to copy and import.

Data & Sheets

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.

Crypto, HTTP Request, Airtable
Data & Sheets

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**.

Airtable, HTTP Request
Data & Sheets

Reel-Analysis-Of-Favourite-Content-Creator. Uses httpRequest, airtable. Scheduled trigger; 26 nodes.

HTTP Request, Airtable
Data & Sheets

Link-By-Reel-Analysis. Uses httpRequest, airtable. Scheduled trigger; 24 nodes.

HTTP Request, Airtable
Data & Sheets

&gt; Transform your content strategy with automated competitor intelligence

HTTP Request, Airtable, Notion +2