AutomationFlowsData & Sheets › 4-Hour Reminder with Verification

4-Hour Reminder with Verification

Original n8n title: Recordatorio 4h (con Verificación) ✅

03 - Recordatorio 4h (CON VERIFICACIÓN) ✅. Uses supabase, httpRequest, twilio. Scheduled trigger; 17 nodes.

Cron / scheduled trigger★★★★☆ complexity17 nodesSupabaseHTTP RequestTwilio
Data & Sheets Trigger: Cron / scheduled Nodes: 17 Complexity: ★★★★☆ Added:

This workflow follows the HTTP Request → Supabase 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": "03 - Recordatorio 4h (CON VERIFICACI\u00d3N) \u2705",
  "nodes": [
    {
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "cronExpression",
              "expression": "0 */2 * * *"
            }
          ]
        }
      },
      "id": "87d9ee29-5029-4fc4-98ff-4543ba25223d",
      "name": "\u23f0 Cron Cada 2 Horas",
      "type": "n8n-nodes-base.scheduleTrigger",
      "typeVersion": 1.1,
      "position": [
        -2016,
        816
      ]
    },
    {
      "parameters": {
        "operation": "getAll",
        "tableId": "reservations",
        "returnAll": true
      },
      "id": "48e4e133-81c8-4560-aa96-33baece19350",
      "name": "\ud83d\udcca Obtener TODAS las Reservas",
      "type": "n8n-nodes-base.supabase",
      "typeVersion": 1,
      "position": [
        -1776,
        816
      ],
      "credentials": {
        "supabaseApi": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "jsCode": "const now = new Date();\nconst in4Hours = new Date(now.getTime() + 4 * 60 * 60 * 1000);\nconst in24Hours = new Date(now.getTime() + 24 * 60 * 60 * 1000);\n\nconst filtered = $input.all().filter(item => {\n  const data = item.json;\n  if (!data.reservation_date || !data.reservation_time) return false;\n  \n  const reservationDateTime = new Date(`${data.reservation_date}T${data.reservation_time}`);\n  \n  return reservationDateTime >= in4Hours &&\n         reservationDateTime <= in24Hours &&\n         data.status === 'pending' &&\n         data.customer_phone !== null &&\n         data.customer_phone !== '';\n});\n\nconsole.log(`\ud83d\udd0d Encontradas ${filtered.length} reservas en 4-24h`);\n\nreturn filtered;"
      },
      "id": "0fa54462-f859-4903-94d6-27bbf65135dc",
      "name": "\ud83d\udd0d Filtrar: 4-24h + Pending",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        -1536,
        816
      ]
    },
    {
      "parameters": {
        "conditions": {
          "boolean": [
            {
              "value1": "={{ $input.all().length }}",
              "value2": 0,
              "operator": {
                "type": "number",
                "operation": "gt"
              }
            }
          ]
        },
        "options": {}
      },
      "id": "07239649-2e68-4ad3-972e-2fae7abce9b2",
      "name": "\u2753 \u00bfHay Reservas?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2,
      "position": [
        -1344,
        816
      ]
    },
    {
      "parameters": {
        "options": {}
      },
      "id": "e91fa27f-d443-4e01-a4a8-95db85454ff6",
      "name": "\ud83d\udd01 Loop Cada Reserva",
      "type": "n8n-nodes-base.splitInBatches",
      "typeVersion": 3,
      "position": [
        -1072,
        608
      ]
    },
    {
      "parameters": {
        "method": "POST",
        "url": "https://ktsqwvhqamedpmzkzjaz.supabase.co/rest/v1/rpc/check_confirmation_sent",
        "authentication": "predefinedCredentialType",
        "nodeCredentialType": "supabaseApi",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "Content-Type",
              "value": "application/json"
            }
          ]
        },
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={\n  \"p_reservation_id\": \"{{ $json.id }}\",\n  \"p_message_type\": \"4h\"\n}",
        "options": {}
      },
      "id": "check-duplicate-4h",
      "name": "\ud83d\udd0d Verificar Si Ya Enviado",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        -848,
        608
      ],
      "credentials": {
        "supabaseApi": {
          "name": "<your credential>"
        }
      },
      "notes": "\u2705 Verifica duplicados v\u00eda RPC"
    },
    {
      "parameters": {
        "jsCode": "const reservation = $('\ud83d\udd01 Loop Cada Reserva').first().json;\nconst checkResult = $input.first().json;\n\nconst alreadySent = checkResult === true;\n\nconsole.log(`\ud83d\udd0d Reserva ${reservation.id}: ${alreadySent ? '\u26a0\ufe0f YA ENVIADO' : '\u2705 PENDIENTE DE ENVIAR'}`);\n\nreturn {\n  ...reservation,\n  already_sent: alreadySent,\n  should_send: !alreadySent\n};"
      },
      "id": "process-check-4h",
      "name": "\ud83d\udd04 Procesar Verificaci\u00f3n",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        -624,
        608
      ]
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict"
          },
          "conditions": [
            {
              "id": "should-send",
              "leftValue": "={{ $json.should_send }}",
              "rightValue": "",
              "operator": {
                "type": "boolean",
                "operation": "true"
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "if-should-send-4h",
      "name": "\u2753 \u00bfEnviar Mensaje?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        -400,
        608
      ]
    },
    {
      "parameters": {
        "jsCode": "const items = $input.all();\n\nconst results = items.map(item => {\n  const data = item.json;\n  let phone = data.customer_phone || '';\n  \n  if (phone && !phone.startsWith('+')) {\n    if (phone.startsWith('34')) {\n      phone = '+' + phone;\n    } else if (phone.startsWith('0034')) {\n      phone = '+' + phone.substring(2);\n    } else {\n      phone = '+34' + phone;\n    }\n  }\n  \n  return {\n    json: {\n      ...data,\n      customer_phone_normalized: phone\n    }\n  };\n});\n\nreturn results;"
      },
      "id": "e4cc417f-51dd-4ea1-b02c-531889b469f1",
      "name": "\ud83d\udcde Normalizar Tel\u00e9fono",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        -176,
        528
      ]
    },
    {
      "parameters": {
        "operation": "getAll",
        "tableId": "restaurants",
        "returnAll": true,
        "filters": {
          "conditions": [
            {
              "keyName": "id",
              "condition": "eq",
              "keyValue": "={{ $json.restaurant_id }}"
            }
          ]
        }
      },
      "id": "42e9ce29-8ec0-41ee-838e-8daf0c549077",
      "name": "\ud83d\udccd Obtener Config Restaurante",
      "type": "n8n-nodes-base.supabase",
      "typeVersion": 1,
      "position": [
        16,
        528
      ],
      "credentials": {
        "supabaseApi": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "operation": "get",
        "tableId": "message_templates",
        "filters": {
          "conditions": [
            {
              "keyName": "restaurant_id",
              "keyValue": "={{ $('\ud83d\udccd Obtener Config Restaurante').first().json.id }}"
            },
            {
              "keyName": "category",
              "keyValue": "confirmacion_4h"
            },
            {
              "keyName": "is_active",
              "keyValue": "true"
            }
          ]
        }
      },
      "id": "a86d27d8-4de4-4c06-a724-17370f153cf9",
      "name": "\ud83d\udcc4 Obtener Plantilla 4h",
      "type": "n8n-nodes-base.supabase",
      "typeVersion": 1,
      "position": [
        208,
        528
      ],
      "credentials": {
        "supabaseApi": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "jsCode": "const templateData = $json;\nconst restaurantData = $('\ud83d\udccd Obtener Config Restaurante').first().json;\nconst reservaData = $('\ud83d\udcde Normalizar Tel\u00e9fono').first().json;\n\nif (!reservaData || !restaurantData) {\n  throw new Error('\u274c Faltan datos de reserva o restaurante');\n}\n\nconst reserva = reservaData;\nconst restaurant = restaurantData;\n\nlet template = templateData?.content_markdown || '';\n\nif (!template || template.trim() === '') {\n  template = `\ud83d\udea8 RECORDATORIO URGENTE \ud83d\udea8\\n\\nHola {{customer_name}}!\\n\\nTu reserva en {{restaurant_name}} es en pocas horas a las {{reservation_time}} para {{party_size}} persona(s).\\n\\n\u00a1Te esperamos pronto!`;\n}\n\nconst variables = {\n  customer_name: reserva.customer_name || 'Cliente',\n  restaurant_name: restaurant.name || 'Restaurante',\n  reservation_time: reserva.reservation_time || '',\n  party_size: (reserva.party_size || 1).toString()\n};\n\nlet message = template;\nfor (const [key, value] of Object.entries(variables)) {\n  const regex = new RegExp(`\\\\{\\\\{${key}\\\\}\\\\}`, 'g');\n  message = message.replace(regex, value);\n}\n\nif (!message || message.trim() === '') {\n  message = `\ud83d\udea8 Hola ${variables.customer_name}! Tu reserva en ${variables.restaurant_name} es HOY a las ${variables.reservation_time}. \u00a1Te esperamos!`;\n}\n\nreturn [{\n  json: {\n    id: reserva.id,\n    restaurant_id: reserva.restaurant_id,\n    customer_id: reserva.customer_id,\n    customer_name: reserva.customer_name,\n    customer_phone: reserva.customer_phone,\n    customer_phone_normalized: reserva.customer_phone_normalized,\n    reservation_date: reserva.reservation_date,\n    reservation_time: reserva.reservation_time,\n    party_size: reserva.party_size,\n    status: reserva.status,\n    restaurant_phone: restaurant.phone,\n    restaurant_name: restaurant.name,\n    message_final: message\n  }\n}];"
      },
      "id": "b0f8c3d2-81d6-40fc-84ae-399d7cd27d0b",
      "name": "\ud83d\udd04 Reemplazar Variables",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        400,
        528
      ]
    },
    {
      "parameters": {
        "from": "={{ $json.restaurant_phone }}",
        "to": "={{ $json.customer_phone_normalized }}",
        "toWhatsapp": true,
        "message": "={{ $json.message_final }}",
        "options": {}
      },
      "id": "fd96261e-a8ef-4e92-a92a-9c4f7fb3cc78",
      "name": "\ud83d\udcf1 Twilio: Enviar WhatsApp",
      "type": "n8n-nodes-base.twilio",
      "typeVersion": 1,
      "position": [
        592,
        528
      ],
      "credentials": {
        "twilioApi": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "operation": "create",
        "tableId": "confirmation_messages",
        "fieldsUi": {
          "fieldValues": [
            {
              "fieldId": "reservation_id",
              "fieldValue": "={{ $('\ud83d\udd04 Reemplazar Variables').first().json.id }}"
            },
            {
              "fieldId": "restaurant_id",
              "fieldValue": "={{ $('\ud83d\udd04 Reemplazar Variables').first().json.restaurant_id }}"
            },
            {
              "fieldId": "message_type",
              "fieldValue": "4h"
            },
            {
              "fieldId": "status",
              "fieldValue": "sent"
            },
            {
              "fieldId": "customer_phone",
              "fieldValue": "={{ $('\ud83d\udd04 Reemplazar Variables').first().json.customer_phone_normalized }}"
            },
            {
              "fieldId": "customer_name",
              "fieldValue": "={{ $('\ud83d\udd04 Reemplazar Variables').first().json.customer_name }}"
            },
            {
              "fieldId": "reservation_date",
              "fieldValue": "={{ $('\ud83d\udd04 Reemplazar Variables').first().json.reservation_date }}"
            },
            {
              "fieldId": "reservation_time",
              "fieldValue": "={{ $('\ud83d\udd04 Reemplazar Variables').first().json.reservation_time }}"
            },
            {
              "fieldId": "message_content",
              "fieldValue": "={{ $('\ud83d\udd04 Reemplazar Variables').first().json.message_final }}"
            }
          ]
        },
        "options": {}
      },
      "id": "4b97b15d-9235-4fb6-a980-eec18a707ae8",
      "name": "\ud83d\udcbe Registrar en confirmation_messages",
      "type": "n8n-nodes-base.supabase",
      "typeVersion": 1,
      "position": [
        784,
        528
      ],
      "credentials": {
        "supabaseApi": {
          "name": "<your credential>"
        }
      },
      "notes": "\u2705 Registro en la tabla del nuevo sistema"
    },
    {
      "parameters": {
        "jsCode": "console.log('\u2705 Mensaje 4h enviado y registrado');\nreturn { success: true };"
      },
      "id": "log-sent-4h",
      "name": "\u2705 Log: Enviado",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        976,
        528
      ]
    },
    {
      "parameters": {
        "jsCode": "console.log('\u26a0\ufe0f Mensaje ya enviado anteriormente - Saltando');\nreturn { skipped: true };"
      },
      "id": "log-skipped-4h",
      "name": "\u26a0\ufe0f Log: Saltado",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        -176,
        688
      ]
    },
    {
      "parameters": {},
      "id": "c5d223e0-cb78-470d-b789-3f73a5f71bbc",
      "name": "\u2705 Fin: Sin Reservas",
      "type": "n8n-nodes-base.noOp",
      "typeVersion": 1,
      "position": [
        -1056,
        1056
      ]
    }
  ],
  "connections": {
    "\u23f0 Cron Cada 2 Horas": {
      "main": [
        [
          {
            "node": "\ud83d\udcca Obtener TODAS las Reservas",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "\ud83d\udcca Obtener TODAS las Reservas": {
      "main": [
        [
          {
            "node": "\ud83d\udd0d Filtrar: 4-24h + Pending",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "\ud83d\udd0d Filtrar: 4-24h + Pending": {
      "main": [
        [
          {
            "node": "\u2753 \u00bfHay Reservas?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "\u2753 \u00bfHay Reservas?": {
      "main": [
        [
          {
            "node": "\ud83d\udd01 Loop Cada Reserva",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "\u2705 Fin: Sin Reservas",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "\ud83d\udd01 Loop Cada Reserva": {
      "main": [
        [],
        [
          {
            "node": "\ud83d\udd0d Verificar Si Ya Enviado",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "\ud83d\udd0d Verificar Si Ya Enviado": {
      "main": [
        [
          {
            "node": "\ud83d\udd04 Procesar Verificaci\u00f3n",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "\ud83d\udd04 Procesar Verificaci\u00f3n": {
      "main": [
        [
          {
            "node": "\u2753 \u00bfEnviar Mensaje?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "\u2753 \u00bfEnviar Mensaje?": {
      "main": [
        [
          {
            "node": "\ud83d\udcde Normalizar Tel\u00e9fono",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "\u26a0\ufe0f Log: Saltado",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "\ud83d\udcde Normalizar Tel\u00e9fono": {
      "main": [
        [
          {
            "node": "\ud83d\udccd Obtener Config Restaurante",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "\ud83d\udccd Obtener Config Restaurante": {
      "main": [
        [
          {
            "node": "\ud83d\udcc4 Obtener Plantilla 4h",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "\ud83d\udcc4 Obtener Plantilla 4h": {
      "main": [
        [
          {
            "node": "\ud83d\udd04 Reemplazar Variables",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "\ud83d\udd04 Reemplazar Variables": {
      "main": [
        [
          {
            "node": "\ud83d\udcf1 Twilio: Enviar WhatsApp",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "\ud83d\udcf1 Twilio: Enviar WhatsApp": {
      "main": [
        [
          {
            "node": "\ud83d\udcbe Registrar en confirmation_messages",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "\ud83d\udcbe Registrar en confirmation_messages": {
      "main": [
        [
          {
            "node": "\u2705 Log: Enviado",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "\u2705 Log: Enviado": {
      "main": [
        [
          {
            "node": "\ud83d\udd01 Loop Cada Reserva",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "\u26a0\ufe0f Log: Saltado": {
      "main": [
        [
          {
            "node": "\ud83d\udd01 Loop Cada Reserva",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "tags": [
    {
      "createdAt": "2025-10-22T19:00:00.000Z",
      "updatedAt": "2025-10-22T19:00:00.000Z",
      "id": "recordatorio-4h-verificacion",
      "name": "\u2705 Con Verificaci\u00f3n de Duplicados"
    }
  ]
}

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.

Pro

For the full experience including quality scoring and batch install features for each workflow upgrade to Pro

About this workflow

03 - Recordatorio 4h (CON VERIFICACIÓN) ✅. Uses supabase, httpRequest, twilio. Scheduled trigger; 17 nodes.

Source: https://github.com/gustausantin/La-ia-app/blob/92b27fcbe408bc47a0a637cacdf245937e053944/n8n/workflows/03-recordatorio-4h-antes-FINAL.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

02 - Recordatorio 24h antes (CON VERIFICACIÓN) ✅. Uses supabase, httpRequest, twilio. Scheduled trigger; 17 nodes.

Supabase, HTTP Request, Twilio
Data & Sheets

This workflow solves a common problem with RSS feeds: they often only provide a short summary or snippet of the full article. This template automatically monitors a list of your favorite blog RSS feed

HTTP Request, RSS Feed Read, Supabase
Data & Sheets

This workflow is a multi-system document synchronization pipeline built in n8n, designed to automatically sync and back up files between Microsoft SharePoint, Supabase/Postgres, and Google Drive.

HTTP Request, Supabase, Postgres +1
Data & Sheets

• Fetches IT-related tenders from the French BOAMP API (filter: informatique) • Scores each tender with OpenAI (pertinence, budget, stack, GO/NO-GO) • Routes to Supabase as hot (≥75) or archived • Run

HTTP Request, Supabase
Data & Sheets

How it works:

HTTP Request, Supabase, N8N Nodes Influencersclub