AutomationFlowsEmail & Gmail › Flujodereservasupdated

Flujodereservasupdated

FlujoDeReservasUpdated. Uses googleSheetsTrigger, googleSheets, googleCalendar, gmail. Event-driven trigger; 27 nodes.

Event trigger★★★★☆ complexity27 nodesGoogle Sheets TriggerGoogle SheetsGoogle CalendarGmail
Email & Gmail Trigger: Event Nodes: 27 Complexity: ★★★★☆ Added:

This workflow follows the Gmail → Google Calendar 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": "FlujoDeReservasUpdated",
  "nodes": [
    {
      "parameters": {
        "pollTimes": {
          "item": [
            {
              "mode": "everyMinute"
            }
          ]
        },
        "documentId": {
          "__rl": true,
          "value": "1n66HEZRlQ2SeqWs0UVmgNoBYkN7gZvjlFvJYdpwwDlw",
          "mode": "list",
          "cachedResultName": "Registro Solicitud",
          "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1n66HEZRlQ2SeqWs0UVmgNoBYkN7gZvjlFvJYdpwwDlw/edit?usp=drivesdk"
        },
        "sheetName": {
          "__rl": true,
          "value": 1989465276,
          "mode": "list",
          "cachedResultName": "Respuestas de formulario 1",
          "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1n66HEZRlQ2SeqWs0UVmgNoBYkN7gZvjlFvJYdpwwDlw/edit#gid=1989465276"
        },
        "event": "rowAdded",
        "options": {}
      },
      "type": "n8n-nodes-base.googleSheetsTrigger",
      "typeVersion": 1,
      "position": [
        -2288,
        544
      ],
      "id": "ac63537c-c48e-4644-909c-5c6527f29e83",
      "name": "DatosFormulario",
      "credentials": {
        "googleSheetsTriggerOAuth2Api": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {},
      "type": "n8n-nodes-base.merge",
      "typeVersion": 3.2,
      "position": [
        -2096,
        720
      ],
      "id": "00f5e0ac-41f0-4220-af35-7082024e3dd4",
      "name": "Merge"
    },
    {
      "parameters": {
        "jsCode": "const r = $json;\n\n// \u2014\u2014 helpers\nfunction pad(n){ return String(n).padStart(2,'0'); }\nfunction parseHora(str){\n  const parts = String(str || '').trim().split(':');\n  return { hh: pad(+parts[0] || 0), mm: pad(+parts[1] || 0) };\n}\nfunction parseFechaSheets(v){\n  if (v == null) return null;\n  if (v instanceof Date && !isNaN(v)) return v;\n  if (typeof v === 'number') {\n    const epoch = Date.UTC(1899,11,30);\n    return new Date(epoch + Math.round(v * 86400000));\n  }\n  const s = String(v).trim();\n  const asDate = new Date(s);\n  if (!isNaN(asDate)) return asDate;\n  const m = s.match(/^(\\d{1,2})[\\/\\-](\\d{1,2})[\\/\\-](\\d{4})$/);\n  if (m) {\n    const dd = +m[1], mm = +m[2] - 1, yyyy = +m[3];\n    return new Date(yyyy, mm, dd);\n  }\n  return null;\n}\n\n// \u2014\u2014 toma la fecha\nconst rawFecha = r['Fecha'] ?? r['Fecha de la reuni\u00f3n'] ?? r['fecha'] ?? r['Date'] ?? r['Marca temporal'] ?? null;\nconst fecha = parseFechaSheets(rawFecha) || new Date();\n\nconst hi = parseHora(r['Inicio'] ?? r['Hora de inicio']);\nconst hf = parseHora(r['Fin']    ?? r['Hora de Termino']);\n\nconst yyyy = fecha.getFullYear();\nconst mm   = pad(fecha.getMonth()+1);\nconst dd   = pad(fecha.getDate());\n\n// Nombres de meses en espa\u00f1ol\nconst meses = [\n  'enero','febrero','marzo','abril','mayo','junio',\n  'julio','agosto','septiembre','octubre','noviembre','diciembre'\n];\n\n// Nueva variable con formato \u201c20 de septiembre del 2025\u201d\nconst fechaLarga = `${dd} de ${meses[fecha.getMonth()]} del ${yyyy}`;\n\n// huso horario fijo\nconst tzOffset = '-03:00';\nconst startIso = `${yyyy}-${mm}-${dd}T${hi.hh}:${hi.mm}:00${tzOffset}`;\nconst endIso   = `${yyyy}-${mm}-${dd}T${hf.hh}:${hf.mm}:00${tzOffset}`;\n\nreturn [{\n  json: {\n    startIso,\n    endIso,\n    summary: `Reuni\u00f3n: ${r['Nombre'] || r['Nombre del solicitante'] || ''}`,\n    description: `\u00c1rea: ${r['\u00c1rea'] || r['\u00c1rea/Departamento'] || ''}`,\n    requesterEmail: r['Correo'] || r['Correo electr\u00f3nico'] || '',\n    participantes: Number(r['N\u00b0 Participantes'] ?? r['N\u00famero de participantes'] ?? 0),\n\n    // para la hoja Registro:\n    nombre: r['Nombre'] || r['Nombre del solicitante'] || '',\n    correo: r['Correo'] || r['Correo electr\u00f3nico'] || '',\n    area:   r['\u00c1rea'] || r['\u00c1rea/Departamento'] || '',\n    fechaStr: `${dd}-${mm}-${yyyy}`,\n    fechaLarga,  // \u2190 aqu\u00ed queda disponible\n    inicioStr: `${hi.hh}:${hi.mm}`,\n    finStr: `${hf.hh}:${hf.mm}`,\n\n    ReservaID: `res-${Date.now()}-${Math.random().toString(16).slice(2,8)}`\n  }\n}];\n"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        -2752,
        704
      ],
      "id": "45a1c71d-f825-4b4c-a693-03256c3d520f",
      "name": "DatosCode"
    },
    {
      "parameters": {
        "operation": "append",
        "documentId": {
          "__rl": true,
          "value": "1n66HEZRlQ2SeqWs0UVmgNoBYkN7gZvjlFvJYdpwwDlw",
          "mode": "list",
          "cachedResultName": "Registro Solicitud",
          "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1n66HEZRlQ2SeqWs0UVmgNoBYkN7gZvjlFvJYdpwwDlw/edit?usp=drivesdk"
        },
        "sheetName": {
          "__rl": true,
          "value": 1534669159,
          "mode": "list",
          "cachedResultName": "Registro",
          "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1n66HEZRlQ2SeqWs0UVmgNoBYkN7gZvjlFvJYdpwwDlw/edit#gid=1534669159"
        },
        "columns": {
          "mappingMode": "defineBelow",
          "value": {
            "Nombre": "={{ $json.nombre }}",
            "Correo": "={{ $json.correo }}",
            "Area/Departamento": "={{ $json.area }}",
            "Fecha": "={{ $json.fecha }}",
            "horaInicio": "={{ $json.inicio }}",
            "horaTermino": "={{ $json.termino }}",
            "N\u00b0 Participantes": "={{ $json.participantes }}",
            "Estado": "En proceso",
            "ID_Reserva": "={{ $json.ReservaID }}"
          },
          "matchingColumns": [],
          "schema": [
            {
              "id": "Nombre",
              "displayName": "Nombre",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "Correo",
              "displayName": "Correo",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "Area/Departamento",
              "displayName": "Area/Departamento",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "Fecha",
              "displayName": "Fecha",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "horaInicio",
              "displayName": "horaInicio",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "horaTermino",
              "displayName": "horaTermino",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "N\u00b0 Participantes",
              "displayName": "N\u00b0 Participantes",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "ID_Reserva",
              "displayName": "ID_Reserva",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true,
              "removed": false
            },
            {
              "id": "Estado",
              "displayName": "Estado",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "Sala asignada",
              "displayName": "Sala asignada",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "ID_Sala",
              "displayName": "ID_Sala",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true,
              "removed": false
            },
            {
              "id": "Event_ID",
              "displayName": "Event_ID",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true,
              "removed": false
            }
          ],
          "attemptToConvertTypes": false,
          "convertFieldsToString": false
        },
        "options": {}
      },
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4.7,
      "position": [
        -2416,
        544
      ],
      "id": "bcaaae7e-ce5c-49e6-9f41-12bb33e51bba",
      "name": "guardarRegistro",
      "credentials": {
        "googleSheetsOAuth2Api": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "documentId": {
          "__rl": true,
          "value": "1n66HEZRlQ2SeqWs0UVmgNoBYkN7gZvjlFvJYdpwwDlw",
          "mode": "list",
          "cachedResultName": "Registro Solicitud",
          "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1n66HEZRlQ2SeqWs0UVmgNoBYkN7gZvjlFvJYdpwwDlw/edit?usp=drivesdk"
        },
        "sheetName": {
          "__rl": true,
          "value": 727732446,
          "mode": "list",
          "cachedResultName": "Salas",
          "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1n66HEZRlQ2SeqWs0UVmgNoBYkN7gZvjlFvJYdpwwDlw/edit#gid=727732446"
        },
        "options": {}
      },
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4.7,
      "position": [
        -2256,
        544
      ],
      "id": "215153c4-513b-42ef-9270-75e5eb29ce38",
      "name": "leerSalas",
      "credentials": {
        "googleSheetsOAuth2Api": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "jsCode": "const reserva = $items()[0].json;\nconst rooms   = $items().slice(1).map(i => i.json);\n\nconst capReq = Number(reserva.participantes || 0);\n\nconst roomsToUse = rooms\n  .map(r => ({\n    room: String(r.Sala || '').trim(),\n    calendarId: String(r.ID || '').trim(),\n    capacity: Number(r.Capacidad || 0),\n    torre: String(r.Ubicacion || '').trim()   // \u2190 a\u00f1adimos la torre\n  }))\n  .filter(r => r.calendarId)\n  .filter(r => r.capacity >= capReq);\n\nreturn roomsToUse.map(r => ({\n  json: {\n    ...reserva,\n    room: r.room,\n    calendarId: r.calendarId,\n    capacity: r.capacity,\n    torre: r.torre                       // \u2190 heredamos torre al flujo\n  }\n}));\n"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        -1952,
        720
      ],
      "id": "773c3c6d-4a23-49b9-9fe7-949d633be120",
      "name": "filtrarCapacidad"
    },
    {
      "parameters": {
        "options": {}
      },
      "type": "n8n-nodes-base.splitInBatches",
      "typeVersion": 3,
      "position": [
        -1776,
        720
      ],
      "id": "79ae0442-1c52-4ada-b9cb-9fb95643686d",
      "name": "Loop Over Items"
    },
    {
      "parameters": {
        "operation": "getAll",
        "calendar": {
          "__rl": true,
          "value": "={{$json.calendarId}}",
          "mode": "id"
        },
        "returnAll": true,
        "timeMin": "={{ $json.startIso }}",
        "timeMax": "={{ $json.endIso }}",
        "options": {}
      },
      "type": "n8n-nodes-base.googleCalendar",
      "typeVersion": 1.3,
      "position": [
        -1552,
        736
      ],
      "id": "0dc44b5b-25b3-4416-abac-743413289424",
      "name": "Get many events",
      "alwaysOutputData": true,
      "credentials": {
        "googleCalendarOAuth2Api": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "jsCode": "// Items del Merge: [ base, ...eventos ]\nconst all = $items();\nconst base = all[0]?.json || {};\nconst events = all.slice(1).map(i => i.json).filter(e => e && (e.start || e.end));\n\nconst reqStart = Date.parse(base.startIso);\nconst reqEnd   = Date.parse(base.endIso);\n\nfunction toRange(e){\n  const s  = e.start?.dateTime || (e.start?.date && (e.start.date + 'T00:00:00Z'));\n  const en = e.end?.dateTime   || (e.end?.date   && (e.end.date   + 'T23:59:59Z'));\n  if (!s || !en) return null;\n  return { start: Date.parse(s), end: Date.parse(en) };\n}\n\nconst ranges = events.map(toRange).filter(Boolean);\nconst isFree = !ranges.some(b => Math.max(reqStart, b.start) < Math.min(reqEnd, b.end));\n\nreturn [{\n  json: {\n    ...base,                 // <- aqu\u00ed vuelven ReservaID, room, calendarId, startIso, endIso\n    checkedEvents: events.length,\n    isFree\n  }\n}];\n"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        -1264,
        832
      ],
      "id": "400a32f3-3691-471c-8983-a09313c92d34",
      "name": "Code in JavaScript"
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict",
            "version": 2
          },
          "conditions": [
            {
              "id": "e02fa576-dad4-469f-bb23-4fa38aaa6b60",
              "leftValue": "={{$json.isFree}}",
              "rightValue": "",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        -1104,
        880
      ],
      "id": "0a6f3021-ebce-43d7-b399-b56171f756f5",
      "name": "If"
    },
    {
      "parameters": {},
      "type": "n8n-nodes-base.merge",
      "typeVersion": 3.2,
      "position": [
        -1392,
        832
      ],
      "id": "cccb1c95-602e-4a68-8c95-c0e5c6fff36b",
      "name": "Merge1"
    },
    {
      "parameters": {},
      "type": "n8n-nodes-base.merge",
      "typeVersion": 3.2,
      "position": [
        -720,
        800
      ],
      "id": "1a4f2cff-6cc3-4677-87fd-822c33673676",
      "name": "Merge2"
    },
    {
      "parameters": {
        "sendTo": "={{ $json.correo }}",
        "subject": "={{ $json.subject }}",
        "message": "={{ $json.htmlBody }}",
        "options": {}
      },
      "type": "n8n-nodes-base.gmail",
      "typeVersion": 2.1,
      "position": [
        -304,
        800
      ],
      "id": "0ad4f8e6-8b3a-44a6-a0f4-0b27ffad584a",
      "name": "Send a message",
      "credentials": {
        "gmailOAuth2": {
          "name": "<your credential>"
        }
      },
      "onError": "continueRegularOutput"
    },
    {
      "parameters": {
        "operation": "update",
        "documentId": {
          "__rl": true,
          "value": "1n66HEZRlQ2SeqWs0UVmgNoBYkN7gZvjlFvJYdpwwDlw",
          "mode": "list",
          "cachedResultName": "Registro Solicitud",
          "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1n66HEZRlQ2SeqWs0UVmgNoBYkN7gZvjlFvJYdpwwDlw/edit?usp=drivesdk"
        },
        "sheetName": {
          "__rl": true,
          "value": 1534669159,
          "mode": "list",
          "cachedResultName": "Registro",
          "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1n66HEZRlQ2SeqWs0UVmgNoBYkN7gZvjlFvJYdpwwDlw/edit#gid=1534669159"
        },
        "columns": {
          "mappingMode": "defineBelow",
          "value": {
            "Sala asignada": "={{ $('Code in JavaScript1').item.json.room }}",
            "Estado": "Asignada",
            "ID_Reserva": "={{ $('Code in JavaScript1').item.json.ReservaID }}",
            "ID_Sala": "={{ $('Code in JavaScript1').item.json.calendarID }}",
            "Event_ID": "={{ $('Code in JavaScript1').item.json.eventID }}"
          },
          "matchingColumns": [
            "ID_Reserva"
          ],
          "schema": [
            {
              "id": "Nombre",
              "displayName": "Nombre",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "Correo",
              "displayName": "Correo",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "Area/Departamento",
              "displayName": "Area/Departamento",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true,
              "removed": false
            },
            {
              "id": "Fecha",
              "displayName": "Fecha",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "horaInicio",
              "displayName": "horaInicio",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "horaTermino",
              "displayName": "horaTermino",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "N\u00b0 Participantes",
              "displayName": "N\u00b0 Participantes",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "ID_Reserva",
              "displayName": "ID_Reserva",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true,
              "removed": false
            },
            {
              "id": "Estado",
              "displayName": "Estado",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "Sala asignada",
              "displayName": "Sala asignada",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "ID_Sala",
              "displayName": "ID_Sala",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true,
              "removed": false
            },
            {
              "id": "Event_ID",
              "displayName": "Event_ID",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true,
              "removed": false
            },
            {
              "id": "row_number",
              "displayName": "row_number",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "number",
              "canBeUsedToMatch": true,
              "readOnly": true,
              "removed": true
            }
          ],
          "attemptToConvertTypes": false,
          "convertFieldsToString": false
        },
        "options": {}
      },
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4.7,
      "position": [
        -96,
        800
      ],
      "id": "97730ec7-7b29-4197-98ef-57f54dea30f9",
      "name": "Update row in sheet",
      "credentials": {
        "googleSheetsOAuth2Api": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "resource": "calendar",
        "calendar": {
          "__rl": true,
          "value": "={{ $json.calendarId }}",
          "mode": "id"
        },
        "timeMin": "={{$json.option.start}}",
        "timeMax": "={{$json.option.end}}",
        "options": {}
      },
      "type": "n8n-nodes-base.googleCalendar",
      "typeVersion": 1.3,
      "position": [
        -1440,
        352
      ],
      "id": "bdcc173b-fdd1-417c-8c09-09e65358f3bc",
      "name": "Get availability in a calendar",
      "alwaysOutputData": true,
      "credentials": {
        "googleCalendarOAuth2Api": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict",
            "version": 2
          },
          "conditions": [
            {
              "id": "73e55d6b-dafc-41fa-9733-b0bdd656d3e4",
              "leftValue": "={{ $json.available }}",
              "rightValue": "",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        -1040,
        448
      ],
      "id": "cf1000e5-91c9-450c-86f8-65cbd15d6ca0",
      "name": "If1",
      "alwaysOutputData": false
    },
    {
      "parameters": {
        "sendTo": "={{ $json.to }}",
        "subject": "={{ $json.subject }}",
        "message": "={{ $json.htmlBody }}",
        "options": {}
      },
      "type": "n8n-nodes-base.gmail",
      "typeVersion": 2.1,
      "position": [
        -640,
        160
      ],
      "id": "06c0992d-294a-45d8-8ce7-b336f158ee09",
      "name": "Send a message1",
      "credentials": {
        "gmailOAuth2": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "mode": "combine",
        "combineBy": "combineByPosition",
        "options": {}
      },
      "type": "n8n-nodes-base.merge",
      "typeVersion": 3.2,
      "position": [
        -1216,
        448
      ],
      "id": "4f30f8ba-d17b-4456-9f0d-0e3e67806026",
      "name": "Merge3"
    },
    {
      "parameters": {
        "includeOtherFields": true,
        "options": {}
      },
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        -1440,
        512
      ],
      "id": "fdb19875-5073-4313-b17d-c65dfbd110b8",
      "name": "SET1"
    },
    {
      "parameters": {
        "includeOtherFields": true,
        "options": {}
      },
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        -1552,
        912
      ],
      "id": "a84c132a-4e31-452e-a553-ddb9db14b5f1",
      "name": "SET"
    },
    {
      "parameters": {
        "includeOtherFields": true,
        "options": {}
      },
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        -896,
        848
      ],
      "id": "438bd56d-3036-4e8e-851c-81f613e051f1",
      "name": "SET2"
    },
    {
      "parameters": {
        "jsCode": "// n8n Code/Function: GenerarAlternativasDesdeLoopDone (parche flex + meta + correo)\n// Soporta: startIso/endIso, calendarId, room | roomName\n// Propaga meta para el HTML/Webhook: descripcion/description, correo, area, ReservID, nombre, participantes.\n\nconst DEFAULT_MIN_MINUTES = 30;\nconst DEFAULT_MAX_MINUTES = 120;\nconst OFFSETS_MINUTES = [-180, -120, -60, 60, 120, 180];\nconst MAX_OPTIONS_PER_ROOM = 8;\n\nconst STEP_LABEL = (m)=> (m<0? `${Math.abs(m)} min antes` : `${m} min despu\u00e9s`);\n\nfunction toDate(x) {\n  if (!x) return null;\n  if (typeof x === 'object' && (x.date || x.dateTime)) {\n    return new Date(x.dateTime || x.date);\n  }\n  return new Date(x);\n}\nfunction toISO(dt) { return new Date(dt).toISOString(); }\nfunction clampDuration(mins, minAllowed, maxAllowed) {\n  return Math.max(minAllowed, Math.min(maxAllowed, mins));\n}\nfunction overlaps(aStart, aEnd, bStart, bEnd) {\n  return (aStart < bEnd) && (bStart < aEnd);\n}\nfunction sameDay(d1, d2) {\n  return d1.getFullYear() === d2.getFullYear() &&\n         d1.getMonth() === d2.getMonth() &&\n         d1.getDate() === d2.getDate();\n}\nfunction shiftMinutes(date, mins) { const d = new Date(date); d.setMinutes(d.getMinutes()+mins); return d; }\nfunction addDays(date, days) { const d = new Date(date); d.setDate(d.getDate()+days); return d; }\n\nfunction normalizeBusyIntervals(j) {\n  if (Array.isArray(j.busyIntervals)) {\n    return j.busyIntervals\n      .map(b => ({ start: toDate(b.start), end: toDate(b.end) }))\n      .filter(b => b.start && b.end && b.start < b.end);\n  }\n  const events = Array.isArray(j.events) ? j.events : [];\n  return events\n    .map(ev => {\n      const s = toDate(ev.start?.dateTime || ev.start?.date);\n      const e = toDate(ev.end?.dateTime || ev.end?.date);\n      return (s && e && s < e) ? { start: s, end: e } : null;\n    })\n    .filter(Boolean);\n}\n\n// ---- helpers correo ----\nfunction getRequesterEmail(localJson) {\n  // 1) del propio \u00edtem\n  let mail = localJson.correo || localJson.email || localJson.requesterEmail || localJson.solicitanteEmail;\n  // 2) intenta leer del nodo de formulario si existe (ajusta el nombre si es distinto)\n  if (!mail) {\n    try {\n      const form = $items('DatosFormulario1', 0, 0);\n      if (form && form.json) {\n        mail = form.json.correo || form.json.email || form.json.mail;\n      }\n    } catch (e) { /* si no existe el nodo, no pasa nada */ }\n  }\n  // 3) limpieza simple\n  if (typeof mail === 'string') {\n    mail = mail.trim();\n    if (mail === '') mail = undefined;\n  }\n  return mail;\n}\n\nconst out = [];\n\nfor (const item of items) {\n  const j = item.json || {};\n\n  // ---- Adaptadores de campos (flex) ----\n  const roomId   = j.roomId || j.calendarId || j.calendar || j.calendario || j.calendarid;\n  const roomName = j.roomName || j.room || j.name || String(roomId || 'Sala');\n\n  // ---- Meta a propagar (tal como viene en tu flujo) ----\n  const descripcion  = j.descripcion || j.description || '';          // ej: \"\u00c1rea: Informatica\"\n  const area         = j.area || j.Area || j['\u00e1rea'] || j['\u00c1rea'] || j.departamento || j.Departamento || '';\n  const ReservID     = j.ReservID || j.reservationId || j.idReserva || j.IDReserva || j.id || j.ReservaID || '';\n  const nombre       = j.nombre || j.solicitanteNombre || j.Nombre || '';\n  const participantes= j.participantes || j.Participantes || j.asistentes || j.participants || '';\n\n  // correo del solicitante (se propagar\u00e1 en la salida)\n  const correo = getRequesterEmail(j) || j.correo || '';\n\n  let reqStart = null, reqEnd = null;\n\n  // 1) request.start/end (si existiera)\n  if (j.request && (j.request.start || j.request.end)) {\n    reqStart = toDate(j.request.start);\n    reqEnd   = toDate(j.request.end);\n  }\n  // 2) requestStart/requestEnd\n  if (!reqStart && j.requestStart) reqStart = toDate(j.requestStart);\n  if (!reqEnd   && j.requestEnd)   reqEnd   = toDate(j.requestEnd);\n  // 3) startIso/endIso\n  if (!reqStart && j.startIso) reqStart = toDate(j.startIso);\n  if (!reqEnd   && j.endIso)   reqEnd   = toDate(j.endIso);\n  // 4) start/end directos\n  if (!reqStart && j.start) reqStart = toDate(j.start);\n  if (!reqEnd   && j.end)   reqEnd   = toDate(j.end);\n\n  // Si no hay hora o no hay sala, saltamos\n  if (!reqStart || !reqEnd || !(reqStart < reqEnd) || !roomId) {\n    continue;\n  }\n\n  // Restricciones\n  const minMinutes = Number.isFinite(Number(j.constraints?.minMinutes))\n    ? Number(j.constraints.minMinutes) : DEFAULT_MIN_MINUTES;\n  const maxMinutes = Number.isFinite(Number(j.constraints?.maxMinutes))\n    ? Number(j.constraints.maxMinutes) : DEFAULT_MAX_MINUTES;\n\n  const requestedDurationMin = Math.round((reqEnd - reqStart) / 60000);\n  const slotDurationMin = clampDuration(requestedDurationMin, minMinutes, maxMinutes);\n\n  // Busy conocido (si no hay, lista vac\u00eda \u2192 luego verificas con GCal)\n  const busy = normalizeBusyIntervals(j);\n\n  // Candidatos mismo d\u00eda\n  const sameDayCandidates = [];\n  for (const off of OFFSETS_MINUTES) {\n    const s = shiftMinutes(reqStart, off);\n    if (!sameDay(s, reqStart)) continue;\n    const e = shiftMinutes(s, slotDurationMin);\n    sameDayCandidates.push({ start: s, end: e, offset: off, dayShift: 0 });\n  }\n\n  // Candidatos +1 d\u00eda\n  const nextDayBase = addDays(reqStart, 1);\n  const nextDayCandidates = [];\n  for (const off of OFFSETS_MINUTES) {\n    const s = shiftMinutes(nextDayBase, off);\n    const e = shiftMinutes(s, slotDurationMin);\n    nextDayCandidates.push({ start: s, end: e, offset: off, dayShift: 1 });\n  }\n\n  const allCandidates = [...sameDayCandidates, ...nextDayCandidates];\n\n  // Filtrar contra busy (si busy=[], pasan todos)\n  const freeOptions = [];\n  outer:\n  for (const c of allCandidates) {\n    for (const b of busy) {\n      if (overlaps(c.start, c.end, b.start, b.end)) continue outer;\n    }\n    freeOptions.push(c);\n    if (freeOptions.length >= MAX_OPTIONS_PER_ROOM) break;\n  }\n\n  for (const opt of freeOptions) {\n    const prettyLabel = (opt.dayShift === 0)\n      ? `Mismo d\u00eda, ${STEP_LABEL(opt.offset)}`\n      : `+1 d\u00eda, ${STEP_LABEL(opt.offset)}`;\n\n    out.push({\n      json: {\n        // ---- Identificaci\u00f3n de sala ----\n        calendarId: roomId,\n        roomId,\n        roomName,\n\n        // ---- Meta propagada (para HTML/Webhook) ----\n        descripcion,\n        description: descripcion,                 // alias en ingl\u00e9s\n        area,\n        ReservID,\n        reservationId: ReservID,                  // alias\n        nombre,\n        solicitanteNombre: nombre,                // alias\n        participantes,\n\n        // ---- Contacto ----\n        correo,\n        to: correo,                               // alias por comodidad\n\n        // ---- Opci\u00f3n propuesta ----\n        option: {\n          start: toISO(opt.start),\n          end: toISO(opt.end),\n          minutes: slotDurationMin,\n          label: prettyLabel,\n          offsetsMinutes: opt.offset,\n          dayShift: opt.dayShift,\n        },\n\n        // ---- Datos de la solicitud original ----\n        request: {\n          start: toISO(reqStart),\n          end: toISO(reqEnd),\n          requestedMinutes: requestedDurationMin,\n          appliedMinutes: slotDurationMin,\n          minMinutes,\n          maxMinutes,\n        },\n      },\n    });\n  }\n}\n\nreturn out;\n"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        -1664,
        352
      ],
      "id": "b66f76a0-9f5a-4b66-8dae-339deb232222",
      "name": "GenerarAlternativas",
      "alwaysOutputData": true
    },
    {
      "parameters": {
        "jsCode": "// Code: Build HTML Alternativas (Run Once for All Items)\n// Con soporte robusto para extraer meta (\u00e1rea, participantes, solicitante, id)\n// Salida: { to, subject, htmlBody }\n\nconst bookingEmail = 'correoparapracticar1@gmail.com';\nconst TZ = 'America/Santiago';\nconst CONFIRM_PREFIX = 'CONFIRMAR:';\nconst WEBHOOK_URL = 'http://localhost:5678/webhook-test/confirmar-reserva';\n\n// ====== Helpers ======\nfunction toLocal(dtStr) {\n  const d = new Date(dtStr);\n  const fecha = d.toLocaleDateString('es-CL', { timeZone: TZ, weekday:'long', day:'2-digit', month:'long', year:'numeric' });\n  const hora  = d.toLocaleTimeString('es-CL', { timeZone: TZ, hour:'2-digit', minute:'2-digit' });\n  return { fecha, hora };\n}\nfunction groupBy(arr, keyFn) {\n  const m = new Map();\n  for (const x of arr) {\n    const k = keyFn(x);\n    if (!m.has(k)) m.set(k, []);\n    m.get(k).push(x);\n  }\n  return m;\n}\nfunction uniqEmails(list) {\n  const emailRegex = /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/;\n  return Array.from(new Set(list.filter(e => e && emailRegex.test(e.trim())).map(e => e.trim())));\n}\nfunction enc(v) { return encodeURIComponent(v == null ? '' : String(v)); }\nfunction fromNode(name){ try{ return $items(name,0,0)?.json || {}; }catch{ return {}; } }\nfunction firstNonEmpty(...vals){ for (const v of vals){ if (v!=null && String(v).trim()!=='') return String(v).trim(); } return ''; }\n\n// ====== Normalizaci\u00f3n de items ======\nconst libres = items.map(i => i.json).filter(j => j?.option?.start && j?.option?.end);\nlibres.sort((a,b) => new Date(a.option.start) - new Date(b.option.start));\n\n// ====== Destinatarios ======\nconst emailsFromItems = items.map(i => (i.json.to || i.json.correo || '')).filter(Boolean);\nlet ref; try { ref = $items('SET1', 0, 0); } catch { ref = null; }\nconst fallback = (ref?.json?.correo || ref?.json?.email || '').trim();\nconst toList = uniqEmails([...emailsFromItems, fallback]);\nconst to = toList.join(', ');\n\n// ===== ULTRA-ROBUSTO: leer meta desde ra\u00edz, request, option y nodos cercanos =====\nconst src   = (items?.[0]?.json) || {};\nconst roots = [\n  src,\n  src.request || {},\n  src.option  || {},\n  src.data    || {},\n  fromNode('DatosCode'),\n  fromNode('DatosFormulario'),\n  fromNode('SET1'),\n  fromNode('guardarRegistro'),\n];\n\n// helper para buscar en m\u00faltiples objetos y variantes de clave\nfunction pick(keys){\n  for (const r of roots){\n    for (const k of keys){\n      if (r?.[k] != null && String(r[k]).trim()!=='') return String(r[k]).trim();\n      if (k.includes('.')){\n        const parts = k.split('.');\n        let cur = r;\n        for (const p of parts){ cur = cur?.[p]; }\n        if (cur != null && String(cur).trim()!=='') return String(cur).trim();\n      }\n    }\n  }\n  return '';\n}\n\n// ==== AQU\u00cd SE DEFINEN TUS 4 CAMPOS ====\nconst area = pick(['area','Area','\u00e1rea','\u00c1rea','departamento','Departamento','request.area','request.departamento']);\nconst participantes = pick(['participantes','Participantes','asistentes','participants','request.participantes','request.asistentes']);\nconst solicitanteNombre = pick(['solicitanteNombre','nombre','Nombre','solicitante','request.solicitanteNombre','request.nombre']);\nconst reservationId = firstNonEmpty(\n  pick(['ReservID','reservationId','reservaId','idReserva','IDReserva','id','ID','request.reservationId','request.id']),\n  'RID'\n);\n\n// ====== Asunto ======\nconst reqStart = libres[0]?.request?.start || null;\nconst reqLocal = reqStart ? toLocal(reqStart) : null;\nconst subject = (libres.length > 0)\n  ? `Alternativas de sala \u2014 ${reqLocal ? `${reqLocal.hora}, ${reqLocal.fecha}` : `${libres.length} opciones`} \u2014 ${reservationId}`\n  : `Sin alternativas disponibles \u2014 ${reservationId}`;\n\n// ====== Construcci\u00f3n HTML ======\nconst byRoom = groupBy(libres, j => j.roomName || j.roomId || 'Sala');\nlet opIndex = 1;\nlet cardsHTML = '';\n\nfor (const [room, arr] of byRoom.entries()) {\n  cardsHTML += `\n    <h2 style=\"font-size:16px; margin:24px 0 8px; color:#222;\">\n      <span style=\"display:inline-block;width:8px;height:8px;border-radius:999px;background:#6c5ce7;margin-right:8px;vertical-align:middle\"></span>\n      ${room} <span style=\"color:#666; font-weight:400; font-size:12px;\">(${arr.length} opci\u00f3n${arr.length>1?'es':''})</span>\n    </h2>`;\n\n  for (const j of arr) {\n    const s = toLocal(j.option.start);\n    const e = toLocal(j.option.end);\n    const dur = j.option.minutes || Math.round((new Date(j.option.end) - new Date(j.option.start))/60000);\n    const roomId = j.roomId || '';\n    const calendarId = j.calendarId || j.roomCalendarId || '';\n    const code = `${reservationId}-OP${String(opIndex).padStart(2,'0')}`;\n    j.confirmCode = code;\n\n    const webhookLink =\n      `${WEBHOOK_URL}`\n      + `?code=${enc(code)}`\n      + `&reservationId=${enc(reservationId)}`\n      + `&room=${enc(room)}`\n      + `&roomId=${enc(roomId)}`\n      + `&calendarId=${enc(calendarId)}`\n      + `&start=${enc(j.option.start)}`\n      + `&end=${enc(j.option.end)}`\n      + `&minutes=${enc(dur)}`\n      + `&requestStart=${enc(reqStart || '')}`\n      + `&to=${enc(to)}`\n      + `&solicitante=${enc(solicitanteNombre)}`\n      + `&area=${enc(area)}`\n      + `&participantes=${enc(participantes)}`;\n\n    cardsHTML += `\n      <div class=\"option\">\n        <div class=\"option-title\">${j.option.label || 'Alternativa'} \u2014 <span style=\"font-weight:700;\">C\u00f3digo: ${code}</span></div>\n        <div class=\"option-details\">\n          <div><strong>Fecha:</strong> ${s.fecha}</div>\n          <div><strong>Horario:</strong> ${s.hora} \u2014 ${e.hora}</div>\n          <div><strong>Duraci\u00f3n:</strong> ${dur} min</div>\n          ${j.capacidad ? `<div><strong>Capacidad:</strong> ${j.capacidad}</div>` : ``}\n          ${j.ubicacion ? `<div><strong>Ubicaci\u00f3n:</strong> ${j.ubicacion}</div>` : ``}\n        </div>\n        <a href=\"${webhookLink}\" target=\"_blank\" class=\"btn-confirm\">Elegir esta sala (confirmar)</a>\n      </div>`;\n    opIndex++;\n  }\n}\n\n// ====== Resumen ======\nconst resumenHTML = `\n  <div class=\"info-grid\">\n    <div class=\"info-box\"><span>\u00c1rea / Departamento</span><strong>${area || '(sin \u00e1rea)'}</strong></div>\n    <div class=\"info-box\"><span>Participantes</span><strong>${participantes || '(sin participantes)'}</strong></div>\n    <div class=\"info-box\"><span>Solicitante</span><strong>${solicitanteNombre || '(sin nombre)'}</strong></div>\n    <div class=\"info-box\"><span>ID de Reserva</span><strong>${reservationId}</strong></div>\n  </div>\n`;\n\n// ====== HTML Final ======\nconst htmlBody = `<!doctype html>\n<html lang=\"es\">\n<head>\n<meta charset=\"utf-8\">\n<title>${subject}</title>\n<style>\n  body {\n    background-color: #F9F6EE;\n    font-family: 'Segoe UI', Roboto, Arial, sans-serif;\n    margin: 0;\n    padding: 40px 16px;\n    color: #111;\n  }\n  .card {\n    max-width: 720px;\n    margin: 0 auto;\n    background: #ffffff;\n    border-radius: 18px;\n    overflow: hidden;\n    box-shadow: 0 4px 20px rgba(0,0,0,0.25);\n  }\n  .header {\n    background: linear-gradient(90deg, #f4b400 0%, #fcd34d 100%);\n    color: #1f1f1f;\n    padding: 24px;\n    display: flex;\n    justify-content: space-between;\n    align-items: center;\n  }\n  .header h1 {\n    margin: 0;\n    font-size: 20px;\n    font-weight: 700;\n  }\n  .estado {\n    background: rgba(255,255,255,0.25);\n    color: #111;\n    padding: 6px 12px;\n    border-radius: 20px;\n    font-size: 13px;\n    font-weight: 600;\n    text-transform: uppercase;\n  }\n  .content {\n    padding: 28px;\n  }\n  .intro {\n    font-size: 15px;\n    color: #222;\n    margin-bottom: 20px;\n    line-height: 1.5;\n  }\n  .info-grid {\n    display: grid;\n    grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));\n    gap: 14px;\n    margin-bottom: 24px;\n  }\n  .info-box {\n    border: 1px solid #e6e6eb;\n    border-radius: 10px;\n    padding: 12px 16px;\n    background: #fafafa;\n  }\n  .info-box span {\n    display: block;\n    font-size: 12px;\n    color: #666;\n    text-transform: uppercase;\n    letter-spacing: 0.5px;\n    margin-bottom: 4px;\n  }\n  .info-box strong {\n    font-size: 15px;\n    color: #111;\n  }\n  .option {\n    border: 1px solid #eee;\n    border-radius: 10px;\n    padding: 18px 20px;\n    margin-bottom: 16px;\n    background: #f9f9f9;\n  }\n  .option-title {\n    font-weight: 700;\n    margin-bottom: 8px;\n    color: #1a1a1a;\n  }\n  .option-details {\n    font-size: 14px;\n    color: #333;\n    line-height: 1.5;\n  }\n  .btn-confirm {\n    display: inline-block;\n    background-color: #111827;\n    color: #fff !important;\n    text-decoration: none;\n    font-weight: 600;\n    border-radius: 8px;\n    padding: 12px 18px;\n    margin-top: 14px;\n    transition: background 0.2s ease-in-out;\n  }\n  .btn-confirm:hover {\n    background-color: #1e293b;\n  }\n  .footer {\n    font-size: 12px;\n    color: #bbb;\n    text-align: center;\n    padding: 20px;\n  }\n</style>\n</head>\n<body>\n  <div class=\"card\">\n    <div class=\"header\">\n      <h1>Alternativas disponibles</h1>\n      <div class=\"estado\">En espera de respuesta</div>\n    </div>\n    <div class=\"content\">\n      <p class=\"intro\">Hola <strong>${solicitanteNombre || 'Usuario'}</strong>, tu solicitud no encontr\u00f3 disponibilidad exacta. \n      A continuaci\u00f3n se muestran las salas alternativas disponibles. Elige la que m\u00e1s te acomode para confirmar tu reserva.</p>\n      ${resumenHTML}\n      ${cardsHTML || `<p style=\"color:#888;\">No se encontraron opciones disponibles.</p>`}\n    </div>\n    <div class=\"footer\">\n      * Horarios expresados en <b>America/Santiago (GMT-3)</b>.<br/>\n      Si ninguna opci\u00f3n te acomoda, responde a este correo indicando un nuevo horario.\n    </div>\n  </div>\n</body>\n</html>`;\n\n// ====== Validaci\u00f3n ======\nif (!to) {\n  return [{ json: { to:'', subject:`[REVISAR] ${subject}`, htmlBody, warning:'No se encontr\u00f3 correo (to)' } }];\n}\n\n// ====== Salida ======\nreturn [{\n  json: {\n    to,\n    subject,\n    htmlBody,\n    totalOptions: libres.length,\n    area,\n    participantes,\n    solicitanteNombre,\n    reservationId\n  }\n}];\n"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        -816,
        304
      ],
      "id": "3cce2011-97e4-45f6-8938-fc95fe5c5691",
      "name": "HTML"
    },
    {
      "parameters": {
        "calendar": {
          "__rl": true,
          "value": "={{ $json.calendarId }}",
          "mode": "id",
          "__regex": "(^[a-zA-Z0-9.!#$%&\u2019*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\\.[a-zA-Z0-9-]+)*)"
        },
        "start": "={{ $json.startIso }}",
        "end": "={{ $json.endIso }}",
        "additionalFields": {
          "attendees": [
            "={{ $json.correo }}"
          ],
          "description": "=ID de la reserva: [{{ $json.ReservaID }}]   Su sala fue confirmada exitosamente. \u00bfCuando? {{ $json.fechaLarga }} de {{ $json.inicioStr }} a {{ $json.finStr }} \u00bfDonde? {{ $json.room }} {{ $json.torre }} Capacidad total: {{ $json.participantes }}  ID de la reserva: [{{$json.ReservaID}}]",
          "summary": "={{ $json.summary }} || {{ $json.room }} || Reservada "
        }
      },
      "type": "n8n-nodes-base.googleCalendar",
      "typeVersion": 1.3,
      "position": [
        -896,
        704
      ],
      "id": "ca485eee-e879-4aad-8354-7b61abb9aa54",
      "name": "Create an event",
      "alwaysOutputData": false,
      "credentials": {
        "googleCalendarOAuth2Api": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "457d6c56-c575-4b50-ac28-eb01a01df333",
              "name": "ReservaID",
              "value": "={{ $json.ReservaID }}",
              "type": "string"
            },
            {
              "id": "6c13aaa1-84d4-47b2-9c52-b499d4976192",
              "name": "nombre",
              "value": "={{ $json.nombre }}",
              "type": "string"
            },
            {
              "id": "04520c70-36b9-493d-ad76-3c88764930c2",
              "name": "area",
              "value": "={{$json.area || $json.Area || $json[\"\u00e1rea\"] || $json[\"\u00c1rea\"] || $json.departamento || $json.Departamento}}",
              "type": "string"
            },
            {
              "id": "25919f02-40d2-4688-89d1-9ab729b32b97",
              "name": "participantes",
              "value": "={{$json.participantes || $json.Participantes || $json.asistentes || $json.participants}}",
              "type": "string"
            },
            {
              "id": "d231763d-a855-46ca-94f0-cc1c1774f093",
              "name": "correo",
              "value": "={{ $json.correo }}",
              "type": "string"
            },
            {
              "id": "b119c7a5-1a29-4f55-8cf3-c6a38a568e65",
              "name": "fecha",
              "value": "={{ $json.fechaStr }}",
              "type": "string"
            },
            {
              "id": "c0f88c80-52ac-4389-9546-cc6583964403",
              "name": "inicio",
              "value": "={{ $json.inicioStr }}",
              "type": "string"
            },
            {
              "id": "bda5dfe3-f395-45ce-b446-735a1db8e3f3",
              "name": "termino",
              "value": "={{ $json.finStr }}",
              "type": "string"
            }
          ]
        },
        "options": {}
      },
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        -2608,
        544
      ],
      "id": "f8aa6156-2a64-4fff-a112-8ad4e00cecc0",
      "name": "SET_META"
    },
    {
      "parameters": {
        "jsCode": "// === CONFIG (usa URL p\u00fablica y /webhook, no /webhook-test) ===\nconst CANCEL_WEBHOOK_URL = 'http://localhost:5678/webhook-test/cancelar-reserva';\n\nconst TZ = 'America/Santiago';\n\n// === INPUTS DESDE Merge/SET ===\nconst d = items[0].json;\nconst e  = items[1].json;\nconst correo       = d.correo;\nconst room         = d.room;\nconst fechaStr     = d.fechaStr;\nconst inicioStr    = d.inicioStr;\nconst finStr       = d.finStr;\nconst summary      = d.summary;\nconst participantes= d.participantes;\nconst nombre       = d.nombre;\nconst ReservaID    = d.ReservaID;   \nconst calendarID = d.calendarId;\nconst eventID = e.id;\n// <- en tu panel se llama as\u00ed\n\nfunction enc(v){ return encodeURIComponent(v == null ? '' : String(v)); }\n\n// Enlace de cancelaci\u00f3n (GET) \u2014 usa tus campos reales\nconst cancelLink =\n  `${CANCEL_WEBHOOK_URL}`\n  + `?accion=cancelar`\n  + `&reservationId=${enc(ReservaID)}`\n  + `&eventId=${enc(eventID)}`\n  + `&calendarId=${enc(calendarID)}`\n  + `&email=${enc(correo)}`\n  + `&room=${enc(room)}`\n  + `&fecha=${enc(fechaStr)}`\n  + `&inicio=${enc(inicioStr)}`\n  + `&fin=${enc(finStr)}`;\n\n\n// Asunto\nconst subject = `\u2705 Reserva confirmada \u2014 ${room || '-'} \u2014 ${ReservaID || ''}`;\n\n// === HTML con bot\u00f3n BULLETPROOF (tabla) ===\nconst htmlBody = `\n<div style=\"font-family: Arial, sans-serif; line-height: 1.6; color: #333333; max-width: 600px; margin: auto; border: 1px solid #dddddd; border-radius: 8px; overflow: hidden;\">\n  <div style=\"background-color: #4CAF50; color: #ffffff; padding: 20px; text-align: center;\">\n    <h1 style=\"margin: 0; font-size: 24px;\">\u2705 Reserva Confirmada Exitosamente</h1>\n  </div>\n\n  <div style=\"padding: 20px;\">\n    <p>Estimado(a) <strong>${nombre || ''}</strong>,</p>\n    <p>\u00a1Tu reserva fue registrada y agregada al calendario!</p>\n\n    <h2 style=\"color: #4CAF50; font-size: 18px; border-bottom: 1px solid #eeeeee; padding-bottom: 5px;\">Detalles de la Reserva</h2>\n\n    <table width=\"100%\" cellspacing=\"0\" cellpadding=\"8\" border=\"0\" style=\"border-collapse: collapse;\">\n      <tbody>\n        <tr style=\"background-color: #f4f4f4;\">\n          <td width=\"30%\" style=\"font-weight: bold; border: 1px solid #dddddd;\">Sala Reservada</td>\n          <td width=\"70%\" style=\"border: 1px solid #dddddd;\"><strong>${room || '-'}</strong></td>\n        </tr>\n        <tr>\n          <td style=\"font-weight: bold; border: 1px solid #dddddd;\">Fecha</td>\n          <td style=\"border: 1px solid #dddddd;\">${fechaStr || '-'}</td>\n        </tr>\n        <tr style=\"background-color: #f4f4f4;\">\n          <td style=\"font-weight: bold; border: 1px solid #dddddd;\">Hora de Inicio</td>\n          <td style=\"border: 1px solid #dddddd;\">${inicioStr || '-'}</td>\n        </tr>\n        <tr>\n          <td style=\"font-weight: bold; border: 1px solid #dddddd;\">Hora de Fin</td>\n          <td style=\"border: 1px solid #dddddd;\">${finStr || '-'}</td>\n        </tr>\n        <tr style=\"background-color: #f4f4f4;\">\n          <td style=\"font-weight: bold; border: 1px solid #dddddd;\">Motivo</td>\n          <td style=\"border: 1px solid #dddddd;\">${summary || '-'}</td>\n        </tr>\n        <tr>\n          <td style=\"font-weight: bold; border: 1px solid #dddddd;\">Participantes</td>\n          <td style=\"border: 1px solid #dddddd;\">${participantes || '-'}</td>\n        </tr>\n      </tbody>\n    </table>\n\n    <p style=\"margin-top: 12px; font-size: 12px; color: #666666;\">\n      <strong>Referencia:</strong> ID de Reserva <b>${ReservaID || '-'}</b>\n    </p>\n\n    <!-- Bot\u00f3n BULLETPROOF (tabla) -->\n    <table role=\"presentation\" cellspacing=\"0\" cellpadding=\"0\" border=\"0\" align=\"center\" style=\"margin:16px auto;\">\n      <tr>\n        <td align=\"center\" bgcolor=\"#e53935\" style=\"\n          border-radius:6px;\n          mso-padding-alt:12px 18px;\n        \">\n          <a href=\"${cancelLink}\"\n             style=\"\n               display:block;\n               padding:12px 18px;\n               font-weight:bold;\n               text-decoration:none;\n               color:#ffffff;\n               font-family: Arial, sans-serif;\n             \">\n            \u274c Cancelar reserva\n          </a>\n        </td>\n      </tr>\n    </table>\n\n    <p style=\"font-size:12px; color:#666; margin-top:10px; text-align:center;\">\n      \u00bfEst\u00e1s viendo esto desde un correo? Si el bot\u00f3n no funciona, usa este enlace:<br/>\n      <a href=\"${cancelLink}\" style=\"color:#e53935; font-weight:bold; word-break:break-all;\">Cancelar mi reserva</a>\n    </p>\n  </div>\n\n  <div style=\"background-color: #f4f4f4; padding: 15px; text-align: center; border-top: 1px solid #dddddd;\">\n    <p style=\"margin: 0; font-size: 12px; color: #666666;\">Sistema de Reservas</p>\n  </div>\n</div>\n`;\n\nreturn [{\n  json: {\n    to: correo || d.emailDestino,\n    subject,\n    htmlBody,\n    ReservaID,\n    room,\n    nombre,\n    participantes,\n    summary,\n    inicioStr,\n    finStr,\n    fechaStr,\n    correo,\n    TZ,\n    calendarID,\n    eventID\n  }\n}];\n"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        -512,
        800
      ],
      "id": "0173fc2b-e73e-410c-91fb-f37f66b073ad",
      "name": "Code in JavaScript1"
    },
    {
      "parameters": {
        "pollTimes": {
          "item": [
            {
              "mode": "everyMinute"
            }
          ]
        },
        "documentId": {
          "__rl": true,
          "value": "1n66HEZRlQ2SeqWs0UVmgNoBYkN7gZvjlFvJYdpwwDlw",
          "mode": "list",
          "cachedResultName": "Registro Solicitud",
          "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1n66HEZRlQ2SeqWs0UVmgNoBYkN7gZvjlFvJYdpwwDlw/edit?usp=drivesdk"
        },
        "sheetName": {
          "__rl": true,
          "value": 1989465276,
          "mode": "list",
          "cachedResultName": "Respuestas de formulario 1",
          "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1n66HEZRlQ2SeqWs0UVmgNoBYkN7gZvjlFvJYdpwwDlw/edit#gid=1989465276"
        },
        "event": "rowAdded",
        "options": {}
      },
      "type": "n8n-nodes-base.googleSheetsTrigger",
      "typeVersion": 1,
      "position": [
        -2928,
        704
      ],
      "id": "b2072bb2-94eb-4dfb-a633-b7c970261df3",
      "name": "DatosFormulario1",
      "credentials": {
        "googleSheetsTriggerOAuth2Api": {
          "name": "<your credential>"
        }
      }
    }
  ],
  "connections": {
    "DatosFormulario": {
      "main": [
        []
      ]
    },
    "DatosCode": {
      "main": [
        [
          {
            "node": "Merge",
            "type": "main",
            "index": 0
          },
          {
            "node": "SET_META",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "guardarRegistro": {
      "main": [
        [
          {
            "node": "leerSalas",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "leerSalas": {
      "main": [
        [
          {
            "node": "Merge",
            "type": "main",
            "index": 1
          }
        ]
      ]
    },
    "Merge": {
      "main": [
        [
          {
            "node": "filtrarCapacidad",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "filtrarCapacidad": {
      "main": [
        [
          {
            "node": "Loop Over Items",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Loop Over Items": {
      "main": [
        [
          {
            "node": "GenerarAlternativas",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Get many events",
            "type": "main",
            "index": 0
          },
          {
            "node": "SET",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get many events": {
      "main": [
        [
          {
            "node": "Merge1",
            "type": "main",
            "index": 1
          }
        ]
      ]
    },
    "Code in JavaScript": {
      "main": [
        [
          {
            "node": "If",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "If": {
      "main": [
        [
          {
            "node": "Create an event",
            "type": "main",
            "index": 0
          },
          {
            "node": "SET2",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Loop Over Items",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Merge1": {
      "main": [
        [
          {
            "node": "Code in JavaScript",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Merge2": {
      "main": [
        [
          {
            "node": "Code in JavaScript1",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Send a message": {
      "main": [
        [
          {
            "node": "Update row in sheet",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get availability in a calendar": {
      "main": [
        [
          {
            "node": "Merge3",
            "type": "main",
            "index": 1
          }
        ]
      ]
    },
    "If1": {
      "main": [
        [
          {
            "node": "HTML",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Merge3": {
      "main": [
        [
          {
            "node": "If1",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "SET1": {
      "main": [
        [
          {
            "node": "Merge3",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "SET": {
      "main": [
        [
          {
            "node": "Merge1",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "SET2": {
      "main": [
        [
          {
            "node": "Merge2",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "GenerarAlternativas": {
      "main": [
        [
          {
            "node": "Get availability in a calendar",
            "type": "main",
            "index": 0
          },
          {
            "node": "SET1",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "HTML": {
      "main": [
        [
          {
            "node": "Send a message1",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Create an event": {
      "main": [
        [
          {
            "node": "Merge2",
            "type": "main",
            "index": 1
          }
        ]
      ]
    },
    "SET_META": {
      "main": [
        [
          {
            "node": "guardarRegistro",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Code in JavaScript1": {
      "main": [
        [
          {
            "node": "Send a message",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "DatosFormulario1": {
      "main": [
        [
          {
            "node": "DatosCode",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "active": false,
  "settings": {
    "executionOrder": "v1"
  },
  "versionId": "88195cd4-ed09-47a6-a8ea-64cdd7ae394f",
  "id": "Xoo4qSpn61iPp8AB",
  "tags": []
}

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

FlujoDeReservasUpdated. Uses googleSheetsTrigger, googleSheets, googleCalendar, gmail. Event-driven trigger; 27 nodes.

Source: https://github.com/juaco323/Practica1/blob/07f8042068fae25c99524c3f9865398f0029d80d/FlujoDeReservasUpdated.json — original creator credit. Request a take-down →

More Email & Gmail workflows → · Browse all categories →

Related workflows

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

Email & Gmail

Automatically processes new orders added to Google Sheets. Small orders are approved instantly; large orders trigger an HTML email with one-click Approve / Reject links — each handled by an independen

Google Sheets Trigger, Google Sheets, Gmail +1
Email & Gmail

General use cases include: Property managers who manage multiple buildings or units. Building owners looking to centralize tenant repair communication. Automation builders who want to learn multi-trig

Google Sheets, Google Drive, Gmail +1
Email & Gmail

Sync your Google Calendar events with Google Sheets and get daily Slack summaries with meeting statistics. FEATURES:

Google Calendar Trigger, Google Sheets, Slack +3
Email & Gmail

Fluxo de Entrevistas. Uses formTrigger, gmail, googleSheets, googleCalendar. Event-driven trigger; 28 nodes.

Form Trigger, Gmail, Google Sheets +1
Email & Gmail

This workflow automates the full offer letter lifecycle, from generation to final candidate response tracking. When a new row with a Pending status is added to Google Sheets, it creates a personalized

Google Sheets Trigger, Google Drive, Google Docs +2