{
  "name": "WhatsApp Business Assistant (Zabio)",
  "connections": {
    "New WhatsApp Message": {
      "main": [
        [
          {
            "node": "Incoming Messages Only",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Incoming Messages Only": {
      "main": [
        [
          {
            "node": "Business Settings",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Business Settings": {
      "main": [
        [
          {
            "node": "Message Type",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Message Type": {
      "main": [
        [
          {
            "node": "Customer Input",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Get Media URL",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Unsupported Message Reply",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Customer Input": {
      "main": [
        [
          {
            "node": "Find Conversation",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get Media URL": {
      "main": [
        [
          {
            "node": "Download Media",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Download Media": {
      "main": [
        [
          {
            "node": "Voice or Image?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Voice or Image?": {
      "main": [
        [
          {
            "node": "Name Audio File",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Prepare Image",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Name Audio File": {
      "main": [
        [
          {
            "node": "Transcribe With Zabio",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Transcribe With Zabio": {
      "main": [
        [
          {
            "node": "Customer Input",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Unsupported Message Reply",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Prepare Image": {
      "main": [
        [
          {
            "node": "Customer Input",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Find Conversation": {
      "main": [
        [
          {
            "node": "Load Conversation State",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Load Conversation State": {
      "main": [
        [
          {
            "node": "Human Handling Active?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Human Handling Active?": {
      "main": [
        [
          {
            "node": "Forward to Staff",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Build Zabio Request",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build Zabio Request": {
      "main": [
        [
          {
            "node": "Ask Zabio",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Ask Zabio": {
      "main": [
        [
          {
            "node": "Read Zabio Answer",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Send Fallback Reply",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Read Zabio Answer": {
      "main": [
        [
          {
            "node": "Reply With Voice?",
            "type": "main",
            "index": 0
          },
          {
            "node": "Save Conversation",
            "type": "main",
            "index": 0
          },
          {
            "node": "Needs a Person?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Reply With Voice?": {
      "main": [
        [
          {
            "node": "Generate Voice Reply",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Send Text Reply",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Generate Voice Reply": {
      "main": [
        [
          {
            "node": "Send Voice Reply",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Send Text Reply",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Needs a Person?": {
      "main": [
        [
          {
            "node": "Alert Staff",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "nodes": [
    {
      "id": "94d7c17c-2ff5-4172-a82b-423589bb54b0",
      "name": "New WhatsApp Message",
      "type": "n8n-nodes-base.whatsAppTrigger",
      "typeVersion": 1,
      "position": [
        0,
        300
      ],
      "parameters": {
        "updates": [
          "messages"
        ],
        "options": {
          "messageStatusUpdates": [
            "failed"
          ]
        }
      },
      "webhookId": "6df4dca9-7d97-4759-b30b-9c52c44b3d2d"
    },
    {
      "id": "f0b52d88-b65a-4813-ae34-518788729782",
      "name": "Incoming Messages Only",
      "type": "n8n-nodes-base.filter",
      "typeVersion": 2.3,
      "position": [
        220,
        300
      ],
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "loose",
            "version": 2
          },
          "conditions": [
            {
              "leftValue": "={{ $json.messages }}",
              "rightValue": "",
              "operator": {
                "type": "array",
                "operation": "notEmpty",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        }
      }
    },
    {
      "id": "c17404e5-0934-4572-aecd-accbcf151c34",
      "name": "Business Settings",
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.5,
      "position": [
        440,
        300
      ],
      "parameters": {
        "mode": "manual",
        "includeOtherFields": false,
        "assignments": {
          "assignments": [
            {
              "id": "bs-name",
              "name": "business_name",
              "value": "Sunrise Dental Clinic",
              "type": "string"
            },
            {
              "id": "bs-profile",
              "name": "business_profile",
              "value": "Location: 123 Main St, Suite 4.\nHours: Monday to Friday 8am to 6pm, Saturday 9am to 1pm, closed Sunday.\nServices and prices: check-up and cleaning $60, consultation $40, teeth whitening $250, fillings from $90.\nBookings: customers can ask for an appointment; staff confirm by WhatsApp within 2 business hours.\nPayment: cash and all major cards.\nCancellations: please give 24 hours notice.\nParking: free parking behind the building.",
              "type": "string"
            },
            {
              "id": "bs-owner",
              "name": "owner_telegram_chat_id",
              "value": "",
              "type": "string"
            },
            {
              "id": "bs-handoff",
              "name": "handoff_hours",
              "value": 12,
              "type": "number"
            },
            {
              "id": "bs-retention",
              "name": "retention_days",
              "value": 30,
              "type": "number"
            },
            {
              "id": "bs-voice-on",
              "name": "voice_replies",
              "value": true,
              "type": "boolean"
            },
            {
              "id": "bs-voices",
              "name": "voices",
              "value": "={{ { \"es\": \"ef_dora\", \"en\": \"af_heart\", \"pt\": \"pf_dora\", \"fr\": \"ff_siwis\", \"it\": \"if_sara\" } }}",
              "type": "object"
            },
            {
              "id": "bs-voice-default",
              "name": "default_voice",
              "value": "af_heart",
              "type": "string"
            },
            {
              "id": "bs-phone",
              "name": "phone",
              "value": "={{ $json.messages[0].from }}",
              "type": "string"
            },
            {
              "id": "bs-contact",
              "name": "contact_name",
              "value": "={{ $json.contacts?.[0]?.profile?.name ?? \"\" }}",
              "type": "string"
            },
            {
              "id": "bs-type",
              "name": "message_type",
              "value": "={{ $json.messages[0].type }}",
              "type": "string"
            },
            {
              "id": "bs-text",
              "name": "text",
              "value": "={{ $json.messages[0].text?.body ?? \"\" }}",
              "type": "string"
            },
            {
              "id": "bs-media",
              "name": "media_id",
              "value": "={{ $json.messages[0].audio?.id ?? $json.messages[0].image?.id ?? \"\" }}",
              "type": "string"
            },
            {
              "id": "bs-caption",
              "name": "caption",
              "value": "={{ $json.messages[0].image?.caption ?? \"\" }}",
              "type": "string"
            },
            {
              "id": "bs-pnid",
              "name": "phone_number_id",
              "value": "={{ $json.metadata.phone_number_id }}",
              "type": "string"
            }
          ]
        }
      }
    },
    {
      "id": "fb55f818-50cb-441f-b875-08bd5baea24c",
      "name": "Message Type",
      "type": "n8n-nodes-base.switch",
      "typeVersion": 3.4,
      "position": [
        660,
        300
      ],
      "parameters": {
        "mode": "rules",
        "rules": {
          "values": [
            {
              "renameOutput": true,
              "outputKey": "Text",
              "conditions": {
                "options": {
                  "caseSensitive": false,
                  "leftValue": "",
                  "typeValidation": "strict",
                  "version": 2
                },
                "conditions": [
                  {
                    "leftValue": "={{ $json.message_type }}",
                    "rightValue": "text",
                    "operator": {
                      "type": "string",
                      "operation": "equals"
                    }
                  }
                ],
                "combinator": "and"
              }
            },
            {
              "renameOutput": true,
              "outputKey": "Voice or Image",
              "conditions": {
                "options": {
                  "caseSensitive": false,
                  "leftValue": "",
                  "typeValidation": "strict",
                  "version": 2
                },
                "conditions": [
                  {
                    "leftValue": "={{ $json.message_type }}",
                    "rightValue": "audio",
                    "operator": {
                      "type": "string",
                      "operation": "equals"
                    }
                  },
                  {
                    "leftValue": "={{ $json.message_type }}",
                    "rightValue": "image",
                    "operator": {
                      "type": "string",
                      "operation": "equals"
                    }
                  }
                ],
                "combinator": "or"
              }
            }
          ]
        },
        "options": {
          "fallbackOutput": "extra",
          "renameFallbackOutput": "Other"
        }
      }
    },
    {
      "id": "cf5d465e-a812-4ea2-ad92-b780d8b42d22",
      "name": "Customer Input",
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.5,
      "position": [
        1980,
        200
      ],
      "parameters": {
        "mode": "manual",
        "includeOtherFields": false,
        "assignments": {
          "assignments": [
            {
              "id": "ci-text",
              "name": "text",
              "value": "={{ $json.text ?? \"\" }}",
              "type": "string"
            },
            {
              "id": "ci-mode",
              "name": "input_mode",
              "value": "={{ ({ audio: \"audio\", image: \"image\" })[$(\"Business Settings\").item.json.message_type] ?? \"text\" }}",
              "type": "string"
            },
            {
              "id": "ci-image",
              "name": "image_data_url",
              "value": "={{ $json.image_data_url ?? \"\" }}",
              "type": "string"
            },
            {
              "id": "ci-cost",
              "name": "transcription_cost_micros",
              "value": "={{ $json.zabio?.cost_micros ?? 0 }}",
              "type": "number"
            }
          ]
        }
      }
    },
    {
      "id": "79b2419c-c589-4a0c-b7f5-4b2b90a8bba7",
      "name": "Get Media URL",
      "type": "n8n-nodes-base.whatsApp",
      "typeVersion": 1.1,
      "position": [
        880,
        460
      ],
      "parameters": {
        "resource": "media",
        "operation": "mediaUrlGet",
        "mediaGetId": "={{ $json.media_id }}"
      },
      "webhookId": "df283785-d5df-4786-aa75-668410a18df2"
    },
    {
      "id": "656b02d1-8ced-4739-a2fb-4ca66feb3904",
      "name": "Download Media",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.5,
      "position": [
        1100,
        460
      ],
      "parameters": {
        "method": "GET",
        "url": "={{ $json.url }}",
        "authentication": "predefinedCredentialType",
        "nodeCredentialType": "whatsAppApi",
        "options": {
          "response": {
            "response": {
              "responseFormat": "file",
              "outputPropertyName": "data"
            }
          }
        }
      }
    },
    {
      "id": "987ddf9b-e83f-4394-9291-402b1b7004c2",
      "name": "Voice or Image?",
      "type": "n8n-nodes-base.switch",
      "typeVersion": 3.4,
      "position": [
        1320,
        460
      ],
      "parameters": {
        "mode": "rules",
        "rules": {
          "values": [
            {
              "renameOutput": true,
              "outputKey": "Voice",
              "conditions": {
                "options": {
                  "caseSensitive": false,
                  "leftValue": "",
                  "typeValidation": "strict",
                  "version": 2
                },
                "conditions": [
                  {
                    "leftValue": "={{ $(\"Business Settings\").item.json.message_type }}",
                    "rightValue": "audio",
                    "operator": {
                      "type": "string",
                      "operation": "equals"
                    }
                  }
                ],
                "combinator": "and"
              }
            },
            {
              "renameOutput": true,
              "outputKey": "Image",
              "conditions": {
                "options": {
                  "caseSensitive": false,
                  "leftValue": "",
                  "typeValidation": "strict",
                  "version": 2
                },
                "conditions": [
                  {
                    "leftValue": "={{ $(\"Business Settings\").item.json.message_type }}",
                    "rightValue": "image",
                    "operator": {
                      "type": "string",
                      "operation": "equals"
                    }
                  }
                ],
                "combinator": "and"
              }
            }
          ]
        }
      }
    },
    {
      "id": "60b29040-0aa0-4a75-8342-5c6221aab5bd",
      "name": "Name Audio File",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1540,
        380
      ],
      "parameters": {
        "mode": "runOnceForEachItem",
        "language": "javaScript",
        "jsCode": "const binary = $input.item.binary;\nbinary.data.fileName = 'voice-note.ogg';\nbinary.data.fileExtension = 'ogg';\nbinary.data.mimeType = 'audio/ogg';\nreturn { json: $input.item.json, binary: binary };"
      }
    },
    {
      "id": "19f14dd4-d796-4f3f-bce2-c140c14df5f4",
      "name": "Transcribe With Zabio",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.5,
      "position": [
        1760,
        380
      ],
      "parameters": {
        "method": "POST",
        "url": "https://api.zabio.ai/v1/audio/transcriptions",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "sendBody": true,
        "contentType": "multipart-form-data",
        "bodyParameters": {
          "parameters": [
            {
              "parameterType": "formBinaryData",
              "name": "file",
              "inputDataFieldName": "data"
            },
            {
              "parameterType": "formData",
              "name": "model",
              "value": "auto"
            }
          ]
        },
        "options": {
          "timeout": 120000
        }
      },
      "onError": "continueErrorOutput"
    },
    {
      "id": "2b9a1690-cf20-4611-a99c-f61229774722",
      "name": "Prepare Image",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1760,
        560
      ],
      "parameters": {
        "mode": "runOnceForAllItems",
        "language": "javaScript",
        "jsCode": "const s = $('Business Settings').first().json;\nconst bin = $input.first().binary.data;\nconst buffer = await this.helpers.getBinaryDataBuffer(0, 'data');\nconst mime = String(bin.mimeType || 'image/jpeg').split(';')[0];\nreturn [{ json: { text: s.caption || '', image_data_url: 'data:' + mime + ';base64,' + buffer.toString('base64') } }];"
      }
    },
    {
      "id": "2b4c94ec-4932-471d-9aaf-9a085034559a",
      "name": "Unsupported Message Reply",
      "type": "n8n-nodes-base.whatsApp",
      "typeVersion": 1.1,
      "position": [
        1540,
        760
      ],
      "parameters": {
        "resource": "message",
        "operation": "send",
        "phoneNumberId": "={{ $(\"Business Settings\").item.json.phone_number_id }}",
        "recipientPhoneNumber": "={{ $(\"Business Settings\").item.json.phone }}",
        "messageType": "text",
        "textBody": "Sorry, I could not process that message. I can read text, photos and screenshots, and listen to voice notes. Please send your question in one of those."
      },
      "onError": "continueRegularOutput",
      "webhookId": "1474c33f-eec8-4e71-8856-7bf8a5621bd0"
    },
    {
      "id": "e27114e1-0b98-45c6-b38a-beca4a8903fe",
      "name": "Find Conversation",
      "type": "n8n-nodes-base.dataTable",
      "typeVersion": 1.1,
      "position": [
        2200,
        200
      ],
      "parameters": {
        "resource": "row",
        "operation": "get",
        "dataTableId": {
          "__rl": true,
          "mode": "name",
          "value": "whatsapp_conversations"
        },
        "matchType": "allConditions",
        "filters": {
          "conditions": [
            {
              "keyName": "phone",
              "condition": "eq",
              "keyValue": "={{ $(\"Business Settings\").item.json.phone }}"
            }
          ]
        },
        "returnAll": false,
        "limit": 1
      },
      "alwaysOutputData": true
    },
    {
      "id": "7a921f5e-354d-4bf9-88eb-6298ffef8adc",
      "name": "Load Conversation State",
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.5,
      "position": [
        2420,
        200
      ],
      "parameters": {
        "mode": "manual",
        "includeOtherFields": false,
        "assignments": {
          "assignments": [
            {
              "id": "ls-conv",
              "name": "conversation_id",
              "value": "={{ $json.conversation_id && $json.updatedAt && (Date.now() - new Date($json.updatedAt).getTime()) < ($(\"Business Settings\").item.json.retention_days - 1) * 86400000 ? $json.conversation_id : \"\" }}",
              "type": "string"
            },
            {
              "id": "ls-handoff",
              "name": "handoff_active",
              "value": "={{ $json.handoff_until ? new Date($json.handoff_until).getTime() > Date.now() : false }}",
              "type": "boolean"
            },
            {
              "id": "ls-total",
              "name": "total_cost_micros",
              "value": "={{ $json.total_cost_micros ?? 0 }}",
              "type": "number"
            },
            {
              "id": "ls-turns",
              "name": "turns",
              "value": "={{ $json.turns ?? 0 }}",
              "type": "number"
            },
            {
              "id": "ls-esc",
              "name": "escalations",
              "value": "={{ $json.escalations ?? 0 }}",
              "type": "number"
            },
            {
              "id": "ls-voice",
              "name": "voice_disclosed",
              "value": "={{ $json.voice_disclosed ?? false }}",
              "type": "boolean"
            }
          ]
        }
      }
    },
    {
      "id": "ad3c338e-20e4-4067-b5aa-38c76736d458",
      "name": "Human Handling Active?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.3,
      "position": [
        2640,
        200
      ],
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "loose",
            "version": 2
          },
          "conditions": [
            {
              "leftValue": "={{ $json.handoff_active }}",
              "rightValue": "",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        }
      }
    },
    {
      "id": "736d6f54-3922-4b49-b7c1-583152e2dfa1",
      "name": "Forward to Staff",
      "type": "n8n-nodes-base.telegram",
      "typeVersion": 1.2,
      "position": [
        2860,
        40
      ],
      "parameters": {
        "resource": "message",
        "operation": "sendMessage",
        "chatId": "={{ $(\"Business Settings\").item.json.owner_telegram_chat_id }}",
        "text": "={{ $(\"Business Settings\").item.json.contact_name }} (+{{ $(\"Business Settings\").item.json.phone }}) wrote while a person is handling the chat:\n\n{{ ({ audio: \"[voice note] \", image: \"[image] \" })[$(\"Customer Input\").item.json.input_mode] ?? \"\" }}{{ $(\"Customer Input\").item.json.text }}",
        "additionalFields": {
          "appendAttribution": false
        }
      },
      "onError": "continueRegularOutput",
      "webhookId": "f564f61d-e8d9-4369-a0fa-f8bf9f585146"
    },
    {
      "id": "5e782b28-861f-43b7-919a-c0b16457284a",
      "name": "Build Zabio Request",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        2860,
        280
      ],
      "parameters": {
        "mode": "runOnceForEachItem",
        "language": "javaScript",
        "jsCode": "const s = $('Business Settings').item.json;\nconst input = $('Customer Input').item.json;\nconst state = $json;\nconst voiceReply = input.input_mode === 'audio' && s.voice_replies;\n\nconst lines = [\n  'You are the WhatsApp assistant for ' + s.business_name + '.',\n  'Only help with questions about this business: its services, prices, hours, location, bookings, orders and policies.',\n  'If the customer asks about anything unrelated to the business, politely say you can only help with questions about ' + s.business_name + '.',\n  'Reply in the language the customer uses. Keep replies short and friendly, suited to WhatsApp: plain text, no headings, no tables.',\n  'Never invent prices, availability or policies. If the business information below does not answer the question, say so and offer to connect the customer with a person.',\n  'Set handoff to true when the customer asks for a person, is upset or complaining, or needs something only staff can do (refunds, custom quotes, confirming a booking).',\n  'Set language to the ISO 639-1 code of the language your reply is written in.'\n];\n\nlet modelInput = input.text;\n\nif (input.input_mode === 'image' && input.image_data_url) {\n  lines.push('The customer sent an image, often a screenshot or a photo. Use what it shows to help with their question about the business. If it has nothing to do with the business, say so politely. Never repeat card numbers, passwords or other sensitive data visible in it.');\n  modelInput = [{\n    role: 'user',\n    content: [\n      { type: 'input_text', text: input.text ? 'Caption from the customer: ' + input.text : 'The customer sent this image with no caption.' },\n      { type: 'input_image', image_url: input.image_data_url }\n    ]\n  }];\n}\n\nif (voiceReply) {\n  lines.push('The customer sent a voice note, transcribed below. Your reply will be sent back as an AI-generated voice message, so write it the way you would say it: under 60 words, no links, no emojis, no lists.');\n  if (!state.voice_disclosed) {\n    lines.push('This is the first voice reply to this customer: start by saying in a few words that you are the AI assistant of ' + s.business_name + '.');\n  }\n} else if (input.input_mode === 'audio') {\n  lines.push('The customer sent a voice note, transcribed below. Reply in text.');\n}\n\nlines.push('', 'Business information:', s.business_profile);\n\nconst body = {\n  model: 'auto',\n  input: modelInput,\n  instructions: lines.join('\\n'),\n  metadata: { channel: 'whatsapp', phone: s.phone, input_mode: input.input_mode },\n  zabio: { session_id: 'wa-' + s.phone, retention_days: s.retention_days },\n  text: {\n    format: {\n      type: 'json_schema',\n      name: 'whatsapp_reply',\n      strict: true,\n      schema: {\n        type: 'object',\n        properties: {\n          reply: { type: 'string' },\n          language: { type: 'string' },\n          handoff: { type: 'boolean' },\n          handoff_reason: { type: 'string' }\n        },\n        required: ['reply', 'language', 'handoff', 'handoff_reason'],\n        additionalProperties: false\n      }\n    }\n  }\n};\n\nif (state.conversation_id) {\n  body.conversation = state.conversation_id;\n}\n\nreturn { json: { body: body } };"
      }
    },
    {
      "id": "f99d78ee-d12e-4a6d-ac74-a743efa67409",
      "name": "Ask Zabio",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.5,
      "position": [
        3080,
        280
      ],
      "parameters": {
        "method": "POST",
        "url": "https://api.zabio.ai/v1/responses",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "sendBody": true,
        "contentType": "json",
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify($json.body) }}",
        "options": {
          "timeout": 180000
        }
      },
      "onError": "continueErrorOutput"
    },
    {
      "id": "5bcb0b63-5d52-45c6-8ba7-7f26f768c581",
      "name": "Read Zabio Answer",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        3300,
        200
      ],
      "parameters": {
        "mode": "runOnceForEachItem",
        "language": "javaScript",
        "jsCode": "const r = $json;\nconst s = $('Business Settings').item.json;\nconst input = $('Customer Input').item.json;\nconst state = $('Load Conversation State').item.json;\n\nlet text = '';\nfor (const item of (r.output || [])) {\n  if (item.type !== 'message') continue;\n  for (const part of (item.content || [])) {\n    if (part.type === 'output_text') text += part.text;\n  }\n}\n\nlet answer;\ntry {\n  answer = JSON.parse(text);\n} catch (e) {\n  answer = { reply: text, language: '', handoff: false, handoff_reason: '' };\n}\n\nconst z = r.zabio || {};\nconst cost = Number(z.cost_micros || 0) + Number(input.transcription_cost_micros || 0);\nconst escalated = Boolean(z.escalated_from);\nconst replyAsVoice = input.input_mode === 'audio' && Boolean(s.voice_replies);\nconst language = String(answer.language || '').toLowerCase().slice(0, 2);\nconst voices = s.voices || {};\nconst prefix = input.input_mode === 'audio' ? '[voice note] ' : (input.input_mode === 'image' ? '[image] ' : '');\nconst handoffUntil = answer.handoff\n  ? new Date(Date.now() + s.handoff_hours * 3600 * 1000).toISOString()\n  : null;\n\nreturn {\n  json: {\n    phone: s.phone,\n    contact_name: s.contact_name,\n    phone_number_id: s.phone_number_id,\n    input_mode: input.input_mode,\n    customer_text: prefix + (input.text || ''),\n    reply: answer.reply || 'Thanks for your message. A person from our team will reply shortly.',\n    language: language,\n    reply_as_voice: replyAsVoice,\n    voice: voices[language] || s.default_voice,\n    voice_disclosed: Boolean(state.voice_disclosed) || replyAsVoice,\n    handoff: Boolean(answer.handoff),\n    handoff_reason: answer.handoff_reason || '',\n    handoff_until: handoffUntil,\n    conversation_id: (r.conversation && r.conversation.id) || state.conversation_id,\n    model_served: z.model_served || r.model || '',\n    escalated: escalated,\n    escalated_from: z.escalated_from || '',\n    cost_micros: cost,\n    total_cost_micros: Number(state.total_cost_micros || 0) + cost,\n    turns: Number(state.turns || 0) + 1,\n    escalations: Number(state.escalations || 0) + (escalated ? 1 : 0)\n  }\n};"
      }
    },
    {
      "id": "e2479277-8aa3-45de-aa26-bb386f6f2bd0",
      "name": "Send Fallback Reply",
      "type": "n8n-nodes-base.whatsApp",
      "typeVersion": 1.1,
      "position": [
        3300,
        500
      ],
      "parameters": {
        "resource": "message",
        "operation": "send",
        "phoneNumberId": "={{ $(\"Business Settings\").item.json.phone_number_id }}",
        "recipientPhoneNumber": "={{ $(\"Business Settings\").item.json.phone }}",
        "messageType": "text",
        "textBody": "Sorry, I am having trouble answering right now. A person from our team will get back to you shortly."
      },
      "onError": "continueRegularOutput",
      "webhookId": "64dc3b46-c466-4860-ac56-d0a2e3f468b6"
    },
    {
      "id": "f539e869-29a9-4671-a7f3-2cd0ab06d851",
      "name": "Reply With Voice?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.3,
      "position": [
        3520,
        0
      ],
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "loose",
            "version": 2
          },
          "conditions": [
            {
              "leftValue": "={{ $json.reply_as_voice }}",
              "rightValue": "",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        }
      }
    },
    {
      "id": "74b4cb31-1e63-4797-8b60-47056d27fef6",
      "name": "Generate Voice Reply",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.5,
      "position": [
        3740,
        -120
      ],
      "parameters": {
        "method": "POST",
        "url": "https://api.zabio.ai/v1/audio/speech",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "sendBody": true,
        "contentType": "json",
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify({ model: \"auto\", input: $json.reply, voice: $json.voice, response_format: \"mp3\" }) }}",
        "options": {
          "timeout": 120000,
          "response": {
            "response": {
              "responseFormat": "file",
              "outputPropertyName": "data"
            }
          }
        }
      },
      "onError": "continueErrorOutput"
    },
    {
      "id": "efc871eb-b7a3-4e6c-b4a9-10ddf497cd7e",
      "name": "Send Voice Reply",
      "type": "n8n-nodes-base.whatsApp",
      "typeVersion": 1.1,
      "position": [
        3960,
        -120
      ],
      "parameters": {
        "resource": "message",
        "operation": "send",
        "phoneNumberId": "={{ $(\"Read Zabio Answer\").item.json.phone_number_id }}",
        "recipientPhoneNumber": "={{ $(\"Read Zabio Answer\").item.json.phone }}",
        "messageType": "audio",
        "mediaPath": "useMedian8n",
        "mediaPropertyName": "data"
      },
      "onError": "continueRegularOutput",
      "webhookId": "a7eb612d-6969-4856-b8e3-87c519d8bcee"
    },
    {
      "id": "e606b7d2-7235-41bc-b6e0-60409250ea01",
      "name": "Send Text Reply",
      "type": "n8n-nodes-base.whatsApp",
      "typeVersion": 1.1,
      "position": [
        3960,
        80
      ],
      "parameters": {
        "resource": "message",
        "operation": "send",
        "phoneNumberId": "={{ $(\"Read Zabio Answer\").item.json.phone_number_id }}",
        "recipientPhoneNumber": "={{ $(\"Read Zabio Answer\").item.json.phone }}",
        "messageType": "text",
        "textBody": "={{ $(\"Read Zabio Answer\").item.json.reply }}"
      },
      "onError": "continueRegularOutput",
      "webhookId": "8691669d-7353-4a49-8193-8ce88a1f95d9"
    },
    {
      "id": "1e0424f4-6ed5-40f8-87e9-a566a71f0f1d",
      "name": "Save Conversation",
      "type": "n8n-nodes-base.dataTable",
      "typeVersion": 1.1,
      "position": [
        3520,
        200
      ],
      "parameters": {
        "resource": "row",
        "operation": "upsert",
        "dataTableId": {
          "__rl": true,
          "mode": "name",
          "value": "whatsapp_conversations"
        },
        "matchType": "allConditions",
        "filters": {
          "conditions": [
            {
              "keyName": "phone",
              "condition": "eq",
              "keyValue": "={{ $json.phone }}"
            }
          ]
        },
        "columns": {
          "mappingMode": "defineBelow",
          "value": {
            "phone": "={{ $json.phone }}",
            "contact_name": "={{ $json.contact_name }}",
            "conversation_id": "={{ $json.conversation_id }}",
            "handoff_until": "={{ $json.handoff_until }}",
            "last_model": "={{ $json.model_served }}",
            "last_cost_micros": "={{ $json.cost_micros }}",
            "total_cost_micros": "={{ $json.total_cost_micros }}",
            "escalations": "={{ $json.escalations }}",
            "turns": "={{ $json.turns }}",
            "voice_disclosed": "={{ $json.voice_disclosed }}"
          },
          "matchingColumns": [],
          "schema": [
            {
              "id": "phone",
              "displayName": "phone",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "readOnly": false,
              "removed": false
            },
            {
              "id": "contact_name",
              "displayName": "contact_name",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "readOnly": false,
              "removed": false
            },
            {
              "id": "conversation_id",
              "displayName": "conversation_id",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "readOnly": false,
              "removed": false
            },
            {
              "id": "handoff_until",
              "displayName": "handoff_until",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "dateTime",
              "readOnly": false,
              "removed": false
            },
            {
              "id": "last_model",
              "displayName": "last_model",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "readOnly": false,
              "removed": false
            },
            {
              "id": "last_cost_micros",
              "displayName": "last_cost_micros",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "number",
              "readOnly": false,
              "removed": false
            },
            {
              "id": "total_cost_micros",
              "displayName": "total_cost_micros",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "number",
              "readOnly": false,
              "removed": false
            },
            {
              "id": "escalations",
              "displayName": "escalations",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "number",
              "readOnly": false,
              "removed": false
            },
            {
              "id": "turns",
              "displayName": "turns",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "number",
              "readOnly": false,
              "removed": false
            },
            {
              "id": "voice_disclosed",
              "displayName": "voice_disclosed",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "boolean",
              "readOnly": false,
              "removed": false
            }
          ]
        }
      },
      "onError": "continueRegularOutput"
    },
    {
      "id": "a4e400f8-58a1-4edb-82a0-8faf43b1c013",
      "name": "Needs a Person?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.3,
      "position": [
        3520,
        400
      ],
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "loose",
            "version": 2
          },
          "conditions": [
            {
              "leftValue": "={{ $json.handoff }}",
              "rightValue": "",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        }
      }
    },
    {
      "id": "7532c9ef-7f97-42fc-a688-34e8d987116f",
      "name": "Alert Staff",
      "type": "n8n-nodes-base.telegram",
      "typeVersion": 1.2,
      "position": [
        3740,
        400
      ],
      "parameters": {
        "resource": "message",
        "operation": "sendMessage",
        "chatId": "={{ $(\"Business Settings\").item.json.owner_telegram_chat_id }}",
        "text": "=A customer needs a person.\n\n{{ $json.contact_name }} (+{{ $json.phone }})\nReason: {{ $json.handoff_reason }}\nLast message: {{ $json.customer_text }}\n\nThe bot stays quiet with this customer for {{ $(\"Business Settings\").item.json.handoff_hours }} hours. Reply to them from your WhatsApp inbox.",
        "additionalFields": {
          "appendAttribution": false
        }
      },
      "onError": "continueRegularOutput",
      "webhookId": "dc4da510-2a65-47f3-9cd0-5fd84f9332e7"
    },
    {
      "id": "a22b16cf-0887-4951-8612-6a28210ca84e",
      "name": "Sticky Note: Overview",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        -60,
        -380
      ],
      "parameters": {
        "content": "## WhatsApp business assistant on Zabio\n\nAnswers customer questions about one business. Text in, text out. Voice note in, voice note out. Photos and screenshots are read and answered in text. Each customer's conversation lives on Zabio, and the chat goes to a person when needed.\n\n**Setup (10 minutes)**\n1. WhatsApp: connect a Meta WhatsApp Cloud API number to the WhatsApp credentials.\n2. Zabio: create a key at zabio.ai and add a Header Auth credential named \"Zabio API key\" with Name `Authorization` and Value `Bearer YOUR_KEY`.\n3. Edit **Business Settings**: name, business information, Telegram chat id for staff alerts, and the voice per language.\n4. Create a data table named exactly `whatsapp_conversations` with these columns: phone, contact_name, conversation_id, handoff_until (date), last_model, last_cost_micros, total_cost_micros, escalations, turns (numbers), voice_disclosed (boolean).\n5. Activate the workflow.\n\nFull guide: https://remarkablecloud.com/docs/automation/whatsapp-business-assistant/\n\n**Calls:** n8n cannot take live calls. Turn on WhatsApp voicemail for your number and recorded voicemails arrive as voice notes, so this workflow answers them with a voice reply.",
        "color": 4,
        "width": 700,
        "height": 640
      }
    },
    {
      "id": "8566b8c4-63c8-4d12-8f4d-a4cc1a3fd77c",
      "name": "Sticky Note: Voice and images",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        840,
        820
      ],
      "parameters": {
        "content": "## Voice notes and images\n\n**Voice:** transcribed with Zabio `/v1/audio/transcriptions` (`model: auto` picks the lowest-cost model for the language), answered, then spoken back with `/v1/audio/speech` as mp3 using the voice set for the reply language in **Business Settings**. The first voice reply to each customer says it comes from an AI assistant, as the voice vendors require. If speech fails, the answer goes out as text. Set `voice_replies` to false to always answer in text.\n\n**Images:** sent to Zabio as `input_image`, so `auto` picks a model that can see. Images stay in the stored conversation and are resent on later turns, which raises the cost of long chats that include photos.",
        "color": 5,
        "width": 1000,
        "height": 300
      }
    },
    {
      "id": "a0a6f5b5-625e-43c4-a3c5-2ce4a0eb2c4c",
      "name": "Sticky Note: Why Zabio",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        2820,
        -380
      ],
      "parameters": {
        "content": "## Why Zabio here\n\n- **No memory node.** Zabio stores the conversation (`conversation_id` per phone), so the history survives restarts and model switches.\n- **Low cost by default.** `model: auto` answers hours and prices on a fast model and only uses a vision model when an image arrives.\n- **Escalates on its own.** If a question is beyond the first model, Zabio re-runs the turn on a stronger one with the full history. `escalated_from` shows when it happened.\n- **One key for text, vision, transcription and voice.** Cost per turn is in the data table.",
        "color": 6,
        "width": 640,
        "height": 320
      }
    },
    {
      "id": "a66a2960-4e37-497d-8d8f-090a86a07246",
      "name": "Sticky Note: Handoff",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        3480,
        600
      ],
      "parameters": {
        "content": "## Human handoff\n\nWhen the customer asks for a person, complains, or needs staff, the bot replies, alerts you on Telegram, and stays quiet with that customer for `handoff_hours`. Messages that arrive meanwhile are forwarded to you. Swap the Telegram nodes for email or Slack if you prefer.",
        "color": 3,
        "width": 480,
        "height": 220
      }
    }
  ],
  "settings": {
    "executionOrder": "v1"
  },
  "pinData": {},
  "meta": {
    "templateCredsSetupCompleted": false
  }
}
