Workflow: develop a modular n8n workflow that supports Code Respondtowebhook Process Webhook
Integrations: code, respondToWebhook, stickyNote, switch, webhook

{"meta": {"instanceId": "32014bf2061907b54debfd6d86e0e8dc3f3ec9cdd9123c339fc7506178206d83", "templateCredsSetupCompleted": true}, "nodes": [{"id": "1874c66a-97f0-4a33-a4e9-ab27b950edb4", "name": "Webhook1", "type": "n8n-nodes-base.webhook", "position": [-1820, 860], "webhookId": "7116a2e3-c07f-4638-9140-3548a7957d15", "parameters": {"path": "flow", "options": {"responseHeaders": {"entries": [{"name": "Content-Type", "value": "text/plain"}]}}, "httpMethod": "POST", "responseMode": "responseNode"}, "typeVersion": 2}, {"id": "ae85225c-addf-44e8-a60f-f9e0f07a9bc0", "name": "Json Parser", "type": "n8n-nodes-base.code", "position": [-1060, 860], "parameters": {"jsCode": "function processPayload(items) {\n  // Create a new array to store the processed items\n  const processedItems = [];\n  \n  // Process each item in the input array\n  for (const item of items) {\n    try {\n      // Extract the decryptedPayload string from the current item\n      const decryptedPayloadString = item.json.decryptedPayload;\n      \n      // Parse the decryptedPayload string into a JavaScript object\n      const decryptedPayloadObject = JSON.parse(decryptedPayloadString);\n      \n      // Extract the date from the data object\n      const date = decryptedPayloadObject.data.date;\n      \n      // Extract the screen value\n      const screen = decryptedPayloadObject.screen;\n\n      // Extract the flow_token object\n      const flow_token = decryptedPayloadObject.flow_token;\n      \n      // Create a new item with the extracted date and screen\n      const newItem = {\n        json: {\n          date: date,\n          screen: screen,\n          flow_token: flow_token,\n          // Optionally preserve original data\n          originalPayload: item.json\n        }\n      };\n      \n      // Add the processed item to our array\n      processedItems.push(newItem);\n    } catch (error) {\n      // If there's an error, create an item with error information\n      processedItems.push({\n        json: {\n          error: error.message,\n          originalItem: item.json\n        }\n      });\n    }\n  }\n  \n  return processedItems;\n}\n\nreturn processPayload(items);"}, "typeVersion": 2}, {"id": "8ee86c97-ed4f-48d1-924f-4252e1c07aa5", "name": "Switch", "type": "n8n-nodes-base.switch", "position": [-740, 860], "parameters": {"rules": {"values": [{"conditions": {"options": {"version": 2, "leftValue": "", "caseSensitive": true, "typeValidation": "strict"}, "combinator": "and", "conditions": [{"id": "aa929857-8458-49da-a027-0b4d4a7f75f7", "operator": {"type": "string", "operation": "equals"}, "leftValue": "={{ $json.screen }}", "rightValue": "APPOINTMENT"}]}}, {"conditions": {"options": {"version": 2, "leftValue": "", "caseSensitive": true, "typeValidation": "strict"}, "combinator": "and", "conditions": [{"id": "d83dd890-5ee5-480e-b338-efc5eb26b494", "operator": {"name": "filter.operator.equals", "type": "string", "operation": "equals"}, "leftValue": "={{ $json.screen }}", "rightValue": "DATE_SELECTION_SCREEN"}]}}]}, "options": {}}, "typeVersion": 3.2}, {"id": "76fad406-2591-4531-acab-01cbfcf41c3f", "name": "Respond to Webhook1", "type": "n8n-nodes-base.respondToWebhook", "position": [40, 760], "parameters": {"options": {"responseCode": 200}, "respondWith": "text", "responseBody": "={{ $json.body }}"}, "typeVersion": 1.1}, {"id": "56cb338a-9d7a-4f1a-9c55-5ca9db4f3560", "name": "Data Extraction Code", "type": "n8n-nodes-base.code", "position": [-400, 760], "parameters": {"jsCode": "const groupedAppointments = items.reduce((acc, { json: { appointment_date, start_time } }) => {\n  const dateKey = new Date(appointment_date).toISOString().split('T')[0];\n  if (!acc[dateKey]) {\n    acc[dateKey] = [];\n  }\n  acc[dateKey].push(start_time);\n  return acc;\n}, {});\n\nreturn Object.entries(groupedAppointments).map(([date, times]) => ({\n  json: { appointment_date: date, start_times: times }\n}));\n"}, "typeVersion": 2}, {"id": "8bd15faf-3a9b-4bb4-ac83-c913a7373480", "name": "Respond to Webhook2", "type": "n8n-nodes-base.respondToWebhook", "position": [40, 1000], "parameters": {"options": {"responseCode": 200}, "respondWith": "text", "responseBody": "={{ $json.body }}"}, "typeVersion": 1.1}, {"id": "67b06ae5-81c1-4efd-993e-a54e36bc5ce7", "name": "Data Extraction Code1", "type": "n8n-nodes-base.code", "position": [-400, 1000], "parameters": {"jsCode": "const 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\n// Return the result properly formatted for n8n\nreturn seats.map(seat => ({ json: { seat } }));\n"}, "typeVersion": 2}, {"id": "2d05f87c-a2c5-4790-9a85-c6cda46db927", "name": "move to base64", "type": "n8n-nodes-base.code", "position": [-1600, 860], "parameters": {"jsCode": "console.log($json);\n\nreturn [\n  {\n    encryptedFlowData: Buffer.from($json.body?.encrypted_flow_data || \"\", \"base64\"),\n    encryptedAesKey: Buffer.from($json.body?.encrypted_aes_key || \"\", \"base64\"),\n    initialVector: Buffer.from($json.body?.initial_vector || \"\", \"base64\"),\n  }\n];\n"}, "typeVersion": 2}, {"id": "760536f8-c3f4-4d24-be36-4ac08004eb48", "name": "Decryption Code", "type": "n8n-nodes-base.code", "position": [-1320, 860], "parameters": {"jsCode": "const crypto = require(\"crypto\");\n\nconst privateKey = `-----BEGIN PRIVATE KEY-----\n[INSERT YOUR KEY HERE]\n-----END PRIVATE KEY-----`;\n\n// Convert input buffers\nconst encryptedAesKeyBuffer = Buffer.from($json.encryptedAesKey.data);\nconst initialVector = Buffer.from($json.initialVector.data);\nconst encryptedFlowData = Buffer.from($json.encryptedFlowData.data);\n\n// Check if encrypted AES key, IV, and encrypted flow data exist\nif (!encryptedAesKeyBuffer || !initialVector || !encryptedFlowData) {\n  throw new Error(\"Missing required data (encrypted AES key, IV, or flow data)\");\n}\n\n// Decrypt AES key using RSA\nconst decryptedKey = crypto.privateDecrypt(\n  {\n    key: privateKey,\n    padding: crypto.constants.RSA_PKCS1_OAEP_PADDING,\n    oaepHash: \"sha256\",\n  },\n  encryptedAesKeyBuffer\n);\n\n// Ensure AES key is exactly 16 bytes (AES-128 requires it)\nconst aesKey = decryptedKey.slice(0, 16);\nif (aesKey.length !== 16) {\n  throw new Error(\"Invalid AES Key length\");\n}\n\n// Handle initialization vector (IV): If needed, flip the IV bits (standardize behavior)\nconst standardizedIv = Buffer.from(initialVector);\nif (standardizedIv.length !== 16) {\n  throw new Error(\"Invalid IV length, must be 16 bytes\");\n}\n\n// Extract the last 16 bytes as the authentication tag (GCM uses 16-byte tags)\nconst authTag = encryptedFlowData.slice(-16);\nconst encryptedDataWithoutTag = encryptedFlowData.slice(0, -16);\n\n// AES Decryption\nconst decipher = crypto.createDecipheriv(\"aes-128-gcm\", aesKey, standardizedIv);\ndecipher.setAuthTag(authTag);\n\nlet decrypted;\ntry {\n  decrypted = Buffer.concat([\n    decipher.update(encryptedDataWithoutTag),\n    decipher.final(),\n  ]);\n} catch (error) {\n  throw new Error(\"Decryption failed: \" + error.message);\n}\n\nreturn [{ \n  decryptedPayload: decrypted.toString(\"utf-8\"),\n  aesKey: aesKey.toString(\"base64\")\n}];\n"}, "typeVersion": 2}, {"id": "17c055f3-c278-48c4-89d4-d305a35bc526", "name": "Encrypt Return", "type": "n8n-nodes-base.code", "position": [-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}, {"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}, {"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### \ud83d\udd17 **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\u2014RSA for key exchange, AES for data encryption.\n\n---\n\n### \ud83e\udde0 **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### \ud83d\udd00 **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\"` \u2192 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### \ud83d\udcc6 **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\u2014typically 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### \ud83e\udde9 **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}], "pinData": {"Webhook1": [{"body": {"initial_vector": "PFfPS7sPwJqYWLySIGWF/Q==", "encrypted_aes_key": "A2BJ/NRN0WsSHZ8KeH1mUreTHICGMprbvh8BP7vEAIyIxeADgtYODJNkJ5P77WsAtJkIx8BwibiWlPfdJlBFaYeQx86hllirf4GygagECsgEJyNX0B98rpx/0eic4FqdR/8bqDWNFZbi7i78vMDG4x+9PArJIwkXWtzuLaLtM2J5j/SAx2y3PV5pKeYqcfg7w/uYlubmkKZjJYuSLmIOHbdO5mmvblDBm8ap5COVvEzK18K3VYyT8BVzawUgfxxhlyCBd7bB36vcS8iKkTl6EFgkPqFmpcCOmZNSmnsJ5tu+e7uRX8OgwryqbFNfb/plZGUPTQJZlrObFO8rw22yJQ==", "encrypted_flow_data": "tkGedq3MER+FadPJh3W6amE18m0x1Xzge6cqPeb5sNkBgOfTtHkRrHuuLjrLG+MvOd9oSzFXdx4sT90cliJSLfp0uUBtVCnBT33Qa5PF87E/iNRtyOCW4Jcp1yv1po54jSVWnVjhgZRCt9akyjBYK1v2YJW5qxarsvFDFsZMsEOOMMOLtOWHGgGGS+tKR5PB7X4WwMHrlCLG9j0yT1U="}, "query": {}, "params": {}, "headers": {"host": "n8n.doubleit.com.br", "accept": "*/*", "connection": "upgrade", "user-agent": "facebookexternalua", "content-type": "application/json", "content-length": "657", "accept-encoding": "deflate, gzip", "x-hub-signature": "sha256=8e8d012f89e53d0a67aa31c19b472636e55b2e86e1569af9b200eb65839a39ce", "x-hub-signature-256": "sha256=5deea4ea13d95f1da43be49579528f5928e29cb7772abd2455d319ff7396df4e"}, "webhookUrl": "https://n8n.doubleit.com.br/webhook/flow", "executionMode": "production"}]}, "connections": {"Switch": {"main": [[{"node": "Data Extraction Code", "type": "main", "index": 0}], [{"node": "Data Extraction Code1", "type": "main", "index": 0}]]}, "Webhook1": {"main": [[{"node": "move to base64", "type": "main", "index": 0}]]}, "Json Parser": {"main": [[{"node": "Switch", "type": "main", "index": 0}]]}, "Encrypt Return": {"main": [[{"node": "Respond to Webhook1", "type": "main", "index": 0}]]}, "move to base64": {"main": [[{"node": "Decryption Code", "type": "main", "index": 0}]]}, "Decryption Code": {"main": [[{"node": "Json Parser", "type": "main", "index": 0}]]}, "Encrypt Return1": {"main": [[{"node": "Respond to Webhook2", "type": "main", "index": 0}]]}, "Data Extraction Code": {"main": [[{"node": "Encrypt Return", "type": "main", "index": 0}]]}, "Data Extraction Code1": {"main": [[{"node": "Encrypt Return1", "type": "main", "index": 0}]]}}}