Zie619/n8n-workflows · error · Error

Seats data is missing or invalid.

Error message

Seats data is missing or invalid.

What it means

Thrown by the 'Encrypt Return1' Code node when the decrypted payload parsed from `items[0].json.originalPayload.decryptedPayload` has no non-empty `data.seats` array. This is a contract check: the SUMMARY screen response requires at least one seat to render.

Source

Thrown at workflows/Code/0924_Code_Respondtowebhook_Process_Webhook.json:234

        -200,
        760
      ],
      "parameters": {
        "jsCode": "const crypto = require(\"crypto\");\n\n// Access initial_vector from the correct path\nconst initialVector = $('move to base64').first().json.initialVector;\n\nif (!initialVector) {\n    throw new Error(\"Initial Vector is undefined or missing.\");\n}\n\n// Check if 'data' is a property of initialVector\nconst ivData = initialVector.data || initialVector; // Fallback to initialVector if no 'data' property\n\nif (!ivData) {\n    throw new Error(\"Initial Vector 'data' is undefined or missing.\");\n}\n\n// Check for various formats of initialVector\nlet ivBuffer;\nif (typeof ivData === \"string\") {\n    ivBuffer = Buffer.from(ivData, 'base64');\n} else if (Buffer.isBuffer(ivData)) {\n    ivBuffer = ivData;\n} else if (Array.isArray(ivData)) {\n    ivBuffer = Buffer.from(ivData);\n} else {\n    throw new Error(\"Initial Vector 'data' is in an unsupported format.\");\n}\n\n// Invert Initialization Vector\nconst invertedIV = Buffer.from(ivBuffer.map((b) => ~b & 0xFF)); // Ensure the result stays a valid byte\n\n// Access AES Key from the correct path\nconst aesKeyBase64 = $('Decryption Code').first().json.aesKey || \"\";\nif (!aesKeyBase64) {\n    throw new Error(\"AES Key is missing.\");\n}\n\nconst aesKey = Buffer.from(aesKeyBase64, \"base64\");\n\n// Extract data from the input with proper error handling\nlet date = \"2025-03-14\"; // Default fallback date\nlet startTimes = []; // Default empty array for start times\n\n// Check if $json exists and has the expected structure\nif ($json) {\n    // Check if $json is an array\n    if (Array.isArray($json) && $json.length > 0) {\n        const appointmentData = $json[0];\n        if (appointmentData && appointmentData.appointment_date) {\n            date = appointmentData.appointment_date;\n        }\n        if (appointmentData && Array.isArray(appointmentData.start_times)) {\n            startTimes = appointmentData.start_times;\n        }\n    } else if ($json.appointment_date) {\n        // If $json is not an array but has appointment_date directly\n        date = $json.appointment_date;\n        if (Array.isArray($json.start_times)) {\n            startTimes = $json.start_times;\n        }\n    }\n}\n\n// Log the structure of $json for debugging\nconsole.log(\"Input JSON structure:\", JSON.stringify($json, null, 2));\n\n// Ensure we have time slots (use defaults if none found)\nif (!startTimes.length) {\n    console.log(\"No time slots found in input, using defaults\");\n    startTimes = [\"12:00:00\", \"12:30:00\", \"13:30:00\", \"14:00:00\"];\n}\n\n// Map the time slots to the required format\nconst timeSlots = startTimes.map((timeString, index) => ({\n    id: `time_${index + 1}`,\n    title: timeString\n}));\n\n// Map the date slots for each time slot\nconst dateSlots = [{\n    id: \"date_1\",\n    title: date\n}];\n\n// Define the response data with the extracted time and date\nconst responseData = {\n    status: \"active\",\n    time: timeSlots,\n    date: dateSlots\n};\n\n// Define the flow_token (accessed from the correct path)\nconst flowToken = $('Json Parser').first().json.flow_token || \"\"; // Fetch the flow_token dynamically from the path\n\nif (!flowToken) {\n    throw new Error(\"Flow token is missing.\");\n}\n\n// Define the next screen (this should be based on your flow logic)\nconst nextScreen = \"APPOINTMENT\"; // You can set this dynamically depending on the flow\n\n// Define Response Message (updated to match the required response format)\nconst responseMessage = JSON.stringify({\n    version: \"3.0\", // Fixed version as per your requirements\n    action: \"data_exchange\", // Since we're responding to a data exchange request\n    screen: nextScreen, // The next screen that the user will be redirected to\n    data: responseData, // Data to send back (includes the time and date)\n    flow_token: flowToken, // Flow token for session identification\n});\n\n// Encrypt Response using AES-GCM\nconst cipher = crypto.createCipheriv(\"aes-128-gcm\", aesKey, invertedIV);\nlet encryptedResponse = Buffer.concat([\n    cipher.update(responseMessage, \"utf-8\"),\n    cipher.final()\n]);\n\n// Get the authentication tag\nconst authTag = cipher.getAuthTag();\n\n// Append the authentication tag to the encrypted response\nconst result = Buffer.concat([encryptedResponse, authTag]);\n\n// Encode the entire response as Base64\nconst base64Response = result.toString(\"base64\");\n\n// Return the Base64-encoded response as the body\nreturn [{ body: base64Response }];\n"
      },
      "typeVersion": 2,
      "notes": "This code node performs automated tasks as part of the workflow."
    },
    {
      "id": "412f55e3-5867-4e65-a494-3e3bf991d59c",
      "name": "Encrypt Return1",
      "type": "n8n-nodes-base.code",
      "position": [
        -200,
        1000
      ],
      "parameters": {
        "jsCode": "const crypto = require(\"crypto\");\n\nconst jsonData = items;\n\n// Parse the decryptedPayload string into a JSON object\nconst decryptedPayload = JSON.parse(jsonData[0].json.originalPayload.decryptedPayload);\n\n// Extract the seats array\nconst seats = decryptedPayload.data.seats;\n\nif (!seats || !Array.isArray(seats) || seats.length === 0) {\n    throw new Error(\"Seats data is missing or invalid.\");\n}\n\n// Access initial_vector from the correct path\nconst initialVector = $('move to base64').first().json.initialVector;\nif (!initialVector) {\n    throw new Error(\"Initial Vector is undefined or missing.\");\n}\n\nconst ivData = initialVector.data || initialVector;\nif (!ivData) {\n    throw new Error(\"Initial Vector 'data' is undefined or missing.\");\n}\n\nlet ivBuffer;\nif (typeof ivData === \"string\") {\n    ivBuffer = Buffer.from(ivData, 'base64');\n} else if (Buffer.isBuffer(ivData)) {\n    ivBuffer = ivData;\n} else if (Array.isArray(ivData)) {\n    ivBuffer = Buffer.from(ivData);\n} else {\n    throw new Error(\"Initial Vector 'data' is in an unsupported format.\");\n}\n\nconst invertedIV = Buffer.from(ivBuffer.map((b) => ~b & 0xFF));\n\n// Access AES Key from the correct path\nconst aesKeyBase64 = $('Decryption Code').first().json.aesKey || \"\";\nif (!aesKeyBase64) {\n    throw new Error(\"AES Key is missing.\");\n}\nconst aesKey = Buffer.from(aesKeyBase64, \"base64\");\n\n// Define the response data with the extracted seats\nconst responseData = {\n    status: \"active\",\n    seats: seats.map((seat, index) => ({\n        id: `seat_${index + 1}`,\n        title: seat\n    }))\n};\n\n// Define the flow_token\nconst flowToken = $('Json Parser').first().json.flow_token || \"\";\nif (!flowToken) {\n    throw new Error(\"Flow token is missing.\");\n}\n\nconst nextScreen = \"SUMMARY\";\n\nconst responseMessage = JSON.stringify({\n    version: \"3.0\",\n    action: \"data_exchange\",\n    screen: nextScreen,\n    data: responseData,\n    flow_token: flowToken,\n});\n\n// Encrypt Response using AES-GCM\nconst cipher = crypto.createCipheriv(\"aes-128-gcm\", aesKey, invertedIV);\nlet encryptedResponse = Buffer.concat([\n    cipher.update(responseMessage, \"utf-8\"),\n    cipher.final()\n]);\n\nconst authTag = cipher.getAuthTag();\nconst result = Buffer.concat([encryptedResponse, authTag]);\nconst base64Response = result.toString(\"base64\");\n\n// Return the encrypted response\nreturn [{ body: base64Response }];\n"
      },
      "typeVersion": 2,
      "notes": "This code node performs automated tasks as part of the workflow."
    },
    {
      "id": "6c130dfe-bec9-4ca5-af1a-9b55ed593b84",
      "name": "Sticky Note",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -2480,
        140
      ],
      "parameters": {
        "width": 580,
        "height": 1900,
        "content": "## Try it out\n\n### 🔗 **1. Webhook Entry & Initial Decryption Block**\n\n**Nodes involved:**\n\n* `Webhook1`\n* `move to base64`\n* `[partially visible node for decryption using RSA + AES]`\n\n**Description:**\n\nThe workflow begins with the `Webhook1` node, which listens for incoming HTTP POST requests. These requests typically contain encrypted data that needs to be decoded to proceed with processing.\n\nOnce received, the `move to base64` node reformats the incoming encrypted components (`encrypted_flow_data`, `encrypted_aes_key`, and `initial_vector`) into binary buffers. These are required inputs for decryption.\n\nThen, the custom JavaScript code (cut off in your snippet) uses a private RSA key to decrypt the AES key, which in turn is used to decrypt the actual data payload (likely using AES-GCM). This is a secure hybrid encryption method—RSA for key exchange, AES for data encryption.\n\n---\n\n### 🧠 **2. Payload Parsing & Preprocessing Block**\n\n**Node involved:**\n\n* `Json Parser`\n\n**Description:**\n\nHere, we take the decrypted JSON payload from Whatsapp Flows and parse key elements from it. This helps standardize and clean the input before deciding what kind of logic or response should follow based on user interaction.\n\n---\n\n### 🔀 **3. Flow Decision Block**\n\n**Node involved:**\n\n* `Switch`\n\n**Description:**\n\nThis decision-making node routes the workflow depending on the screen context extracted earlier.\n\nE.g., If the screen where the user is exchanging information is appointment date:\n\n* `\"APPOINTMENT\"` → follow the logic that handles scheduling data.\n\nThis allows dynamic routing within the workflow, making it adaptable to different user journey steps or screens.\n\n---\n\n### 📆 **4. Appointment Data Handling Block**\n\n**Nodes involved:**\n\n* `Data Extraction Code`\n* `Respond to Webhook1`\n\n**Description:**\n\nWhen the screen is `\"APPOINTMENT\"`, the `Data Extraction Code` node processes appointment data—typically grouping appointment slots by date. This is useful for summarizing available times, perhaps to show a user a calendar view of options.\n\nThe results are then sent back as a plain text response using `Respond to Webhook1`, which finalizes the API call and ensures a secure end-to-end interaction using Whatsapp Flows.\n\n\n### 🧩 **Summary**\n\nThis n8n workflow handles encrypted user interactions and adapts dynamically based on the screen or step the user is currently in. Here's the general pattern:\n\n1. **Webhook receives encrypted data**\n2. **Data is decrypted using hybrid RSA-AES encryption**\n3. **Parsed to extract the current step (`screen`)**\n4. **Conditional logic decides which path to follow**\n5. **Extracts relevant information (e.g., appointments)**\n6. **Returns response back to the user interface or chatbot**\n"
      },
      "typeVersion": 1,

View on GitHub (pinned to 94007c1445)

Solutions

  1. Log decryptedPayload right after parse to confirm the screen/action and the actual location of seat data (it may be data.seat_selection or nested differently).
  2. Make seat selection required in the Flow JSON (mark the seat component required) so a submit without seats is blocked client-side.
  3. Add routing (IF/Switch on action or screen) so Encrypt Return1 only executes for the seat-selection completion action.
  4. If empty seats is a legitimate state, return a friendly error response instead of throwing so the flow does not break.

Example fix

// before
const seats = decryptedPayload.data.seats;
if (!seats || !Array.isArray(seats) || seats.length === 0) {
  throw new Error("Seats data is missing or invalid.");
}

// after
const seats = decryptedPayload?.data?.seats;
if (!Array.isArray(seats) || seats.length === 0) {
  throw new Error(`Seats data is missing for action=${decryptedPayload?.action} screen=${decryptedPayload?.screen} - check Flow payload schema`);
}
Defensive patterns

Strategy: type-guard

Validate before calling

let decryptedPayload;
try { decryptedPayload = JSON.parse(jsonData[0].json.originalPayload.decryptedPayload); }
catch (e) { throw new Error(`decryptedPayload is not valid JSON: ${e.message}`); }
const seats = decryptedPayload?.data?.seats;

Type guard

const isNonEmptySeatArray = (s) => Array.isArray(s) && s.length > 0 && s.every(x => x !== null && x !== undefined);

Try / catch

try { JSON.parse(str); } catch (e) { throw new Error(`Bad decryptedPayload JSON: ${e.message}`); }

Prevention

When it happens

Trigger: JSON.parse of decryptedPayload succeeded but the object's data.seats is undefined, null, not an array, or an empty array; the user submitted the previous screen without selecting any seat; the upstream flow sent a payload for a different screen (e.g. data_exchange for APPOINTMENT) that has no seats field.

Common situations: Seat-selection step optional in the Flow JSON so users can skip it; screen routing mismatch where Encrypt Return1 runs for a non-seat action; payload schema drift after editing the Flow definition; test payloads hand-crafted without seats.

Related errors


AI-assisted analysis of Zie619/n8n-workflows@94007c1445 (2026-08-15). Data as JSON: /api/errors/d657e651f6ac91b5. Report an issue: GitHub.