{
  "name": "Messenger Responder (FB + IG)",
  "nodes": [
    {
      "parameters": {
        "httpMethod": "POST",
        "path": "meta-messenger",
        "responseMode": "responseNode",
        "options": {
          "rawBody": true
        }
      },
      "id": "b1000000-0000-4000-8000-000000000001",
      "name": "Webhook",
      "type": "n8n-nodes-base.webhook",
      "typeVersion": 2,
      "position": [
        240,
        460
      ]
    },
    {
      "parameters": {
        "jsCode": "// Extract Message \u2014 works for both Facebook Messenger and Instagram DMs\n// Meta sends the same webhook structure for both platforms.\n// Instagram entries have messaging[].recipient.id matching your IG page-scoped ID\n// and the object field is 'instagram' vs 'page' for Messenger.\n\nconst body = $input.item.json.body;\n\nconst objectType = body.object; // 'page' for FB Messenger, 'instagram' for IG\nconst entry = body.entry && body.entry[0];\n\nif (!entry) {\n  return [{ json: { error: 'No entry in payload', skip: true } }];\n}\n\nconst messagingEvent = entry.messaging && entry.messaging[0];\n\nif (!messagingEvent || !messagingEvent.message) {\n  // Could be a delivery/read receipt \u2014 ignore\n  return [{ json: { skip: true, reason: 'No message event (delivery/read receipt)' } }];\n}\n\nconst senderId = messagingEvent.sender.id;\nconst recipientId = messagingEvent.recipient.id;\nconst timestamp = messagingEvent.timestamp;\nconst text = messagingEvent.message.text || '';\nconst messageId = messagingEvent.message.mid;\n\n// Determine channel\nlet channel;\nif (objectType === 'instagram') {\n  channel = 'instagram';\n} else {\n  channel = 'facebook_messenger';\n}\n\nreturn [{\n  json: {\n    senderId,\n    recipientId,\n    text,\n    messageId,\n    timestamp,\n    channel,\n    skip: false\n  }\n}];\n"
      },
      "id": "b1000000-0000-4000-8000-000000000002",
      "name": "Extract Message",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        480,
        460
      ]
    },
    {
      "parameters": {
        "respondWith": "json",
        "responseBody": "={\"status\":\"ok\"}",
        "options": {
          "responseCode": 200
        }
      },
      "id": "b1000000-0000-4000-8000-000000000003",
      "name": "Respond OK",
      "type": "n8n-nodes-base.respondToWebhook",
      "typeVersion": 1.1,
      "position": [
        480,
        660
      ]
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict"
          },
          "conditions": [
            {
              "id": "skip-check",
              "leftValue": "={{ $json.skip }}",
              "rightValue": true,
              "operator": {
                "type": "boolean",
                "operation": "notEqual"
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "b1000000-0000-4000-8000-000000000004",
      "name": "If Has Message",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2,
      "position": [
        720,
        460
      ]
    },
    {
      "parameters": {
        "method": "POST",
        "url": "https://api.anthropic.com/v1/messages",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "x-api-key",
              "value": "={{ $credentials.httpHeaderAuth.value }}"
            },
            {
              "name": "anthropic-version",
              "value": "2023-06-01"
            },
            {
              "name": "anthropic-beta",
              "value": "prompt-caching-2024-07-31"
            },
            {
              "name": "Content-Type",
              "value": "application/json"
            }
          ]
        },
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={\n  \"model\": \"claude-haiku-4-5-20251001\",\n  \"max_tokens\": 300,\n  \"temperature\": 0.3,\n  \"system\": [{\"type\": \"text\", \"text\": \"SYSTEM_PROMPT_PLACEHOLDER\", \"cache_control\": {\"type\": \"ephemeral\"}}],\n  \"messages\": [\n    {\n      \"role\": \"user\",\n      \"content\": \"Channel: {{ $json.channel }}\\nSender ID: {{ $json.senderId }}\\nMessage: {{ $json.text }}\"\n    }\n  ]\n}",
        "options": {
          "timeout": 30000,
          "response": {
            "response": {
              "responseFormat": "json"
            }
          }
        }
      },
      "id": "b1000000-0000-4000-8000-000000000005",
      "name": "Claude API",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        960,
        360
      ],
      "credentials": {
        "httpHeaderAuth": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "jsCode": "// Parse Claude's response.\n// Expected format in the assistant reply:\n//   ---REPLY---\n//   <the text to send back to the user>\n//   ---META---\n//   action: reply_only | rsvp | escalate\n//   event_name: <optional, for rsvp>\n//   event_date: <optional, for rsvp>\n//   escalate_reason: <optional, for escalate>\n\nconst content = $input.item.json.content;\nconst assistantText = content && content[0] && content[0].text ? content[0].text : '';\n\n// Preserve the upstream fields\nconst senderId = $('Extract Message').item.json.senderId;\nconst channel = $('Extract Message').item.json.channel;\nconst text = $('Extract Message').item.json.text;\nconst timestamp = $('Extract Message').item.json.timestamp;\n\nlet replyText = assistantText;\nlet action = 'reply_only';\nlet eventName = '';\nlet eventDate = '';\nlet escalateReason = '';\n\nconst replySplit = assistantText.split('---REPLY---');\nif (replySplit.length > 1) {\n  const afterReply = replySplit[1];\n  const metaSplit = afterReply.split('---META---');\n  replyText = metaSplit[0].trim();\n\n  if (metaSplit.length > 1) {\n    const metaBlock = metaSplit[1].trim();\n    const lines = metaBlock.split('\\n');\n    for (const line of lines) {\n      const [key, ...vals] = line.split(':');\n      const k = key.trim().toLowerCase();\n      const v = vals.join(':').trim();\n      if (k === 'action') action = v;\n      if (k === 'event_name') eventName = v;\n      if (k === 'event_date') eventDate = v;\n      if (k === 'escalate_reason') escalateReason = v;\n    }\n  }\n}\n\nreturn [{\n  json: {\n    senderId,\n    channel,\n    incomingText: text,\n    replyText,\n    action,\n    eventName,\n    eventDate,\n    escalateReason,\n    timestamp\n  }\n}];\n"
      },
      "id": "b1000000-0000-4000-8000-000000000006",
      "name": "Parse Response",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1200,
        360
      ]
    },
    {
      "parameters": {
        "rules": {
          "values": [
            {
              "conditions": {
                "conditions": [
                  {
                    "leftValue": "={{ $json.action }}",
                    "rightValue": "rsvp",
                    "operator": {
                      "type": "string",
                      "operation": "equals"
                    }
                  }
                ],
                "combinator": "and"
              },
              "renameOutput": "RSVP"
            },
            {
              "conditions": {
                "conditions": [
                  {
                    "leftValue": "={{ $json.action }}",
                    "rightValue": "escalate",
                    "operator": {
                      "type": "string",
                      "operation": "equals"
                    }
                  }
                ],
                "combinator": "and"
              },
              "renameOutput": "Escalate"
            },
            {
              "conditions": {
                "conditions": [
                  {
                    "leftValue": "={{ $json.action }}",
                    "rightValue": "reply_only",
                    "operator": {
                      "type": "string",
                      "operation": "equals"
                    }
                  }
                ],
                "combinator": "and"
              },
              "renameOutput": "Reply Only"
            }
          ],
          "fallbackOutput": {
            "label": "Reply Only (Fallback)"
          }
        },
        "options": {
          "allMatchingOutputs": false
        }
      },
      "id": "b1000000-0000-4000-8000-000000000007",
      "name": "Route Action",
      "type": "n8n-nodes-base.switch",
      "typeVersion": 3.2,
      "position": [
        1440,
        360
      ]
    },
    {
      "parameters": {
        "method": "POST",
        "url": "https://graph.facebook.com/v19.0/me/messages",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpQueryAuth",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={\n  \"recipient\": {\n    \"id\": \"{{ $json.senderId }}\"\n  },\n  \"message\": {\n    \"text\": \"{{ $json.replyText }}\"\n  },\n  \"messaging_type\": \"RESPONSE\"\n}",
        "options": {
          "timeout": 10000
        }
      },
      "id": "b1000000-0000-4000-8000-000000000008",
      "name": "Send Reply",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        1920,
        560
      ],
      "credentials": {
        "httpQueryAuth": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "documentId": {
          "__rl": true,
          "value": "YOUR_GOOGLE_SHEET_ID",
          "mode": "id"
        },
        "sheetName": {
          "__rl": true,
          "value": "Sheet1",
          "mode": "name"
        },
        "columns": {
          "mappingMode": "defineBelow",
          "value": {
            "timestamp": "={{ $json.timestamp }}",
            "channel": "={{ $json.channel }}",
            "sender_id": "={{ $json.senderId }}",
            "incoming_message": "={{ $json.incomingText }}",
            "reply": "={{ $json.replyText }}",
            "action": "={{ $json.action }}",
            "event_name": "={{ $json.eventName }}",
            "escalate_reason": "={{ $json.escalateReason }}"
          },
          "matchingColumns": [],
          "schema": [
            {
              "id": "timestamp",
              "displayName": "timestamp",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "channel",
              "displayName": "channel",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "sender_id",
              "displayName": "sender_id",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "incoming_message",
              "displayName": "incoming_message",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": false
            },
            {
              "id": "reply",
              "displayName": "reply",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": false
            },
            {
              "id": "action",
              "displayName": "action",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "event_name",
              "displayName": "event_name",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": false
            },
            {
              "id": "escalate_reason",
              "displayName": "escalate_reason",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": false
            }
          ]
        },
        "options": {}
      },
      "id": "b1000000-0000-4000-8000-000000000009",
      "name": "Log to Google Sheet",
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4.5,
      "position": [
        2160,
        560
      ],
      "credentials": {
        "googleSheetsOAuth2Api": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "method": "POST",
        "url": "https://www.googleapis.com/calendar/v3/calendars/primary/events",
        "authentication": "genericCredentialType",
        "genericAuthType": "oAuth2Api",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={\n  \"summary\": \"RSVP: {{ $json.eventName }}\",\n  \"description\": \"RSVP from {{ $json.channel }} user {{ $json.senderId }}. Original message: {{ $json.incomingText }}\",\n  \"start\": {\n    \"dateTime\": \"{{ $json.eventDate }}T09:00:00\",\n    \"timeZone\": \"Europe/Lisbon\"\n  },\n  \"end\": {\n    \"dateTime\": \"{{ $json.eventDate }}T10:00:00\",\n    \"timeZone\": \"Europe/Lisbon\"\n  }\n}",
        "options": {
          "timeout": 10000
        }
      },
      "id": "b1000000-0000-4000-8000-000000000010",
      "name": "Google Calendar RSVP",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        1680,
        200
      ],
      "credentials": {
        "oAuth2Api": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "fromEmail": "oporto.toastmasters.club@gmail.com",
        "toEmail": "oporto.toastmasters.club@gmail.com",
        "subject": "=Escalation from {{ $json.channel }} \u2014 {{ $json.senderId }}",
        "emailType": "text",
        "message": "=A message has been escalated.\n\nChannel: {{ $json.channel }}\nSender ID: {{ $json.senderId }}\nOriginal message: {{ $json.incomingText }}\n\nReason: {{ $json.escalateReason }}\n\nAuto-reply sent:\n{{ $json.replyText }}",
        "options": {}
      },
      "id": "b1000000-0000-4000-8000-000000000011",
      "name": "Email Escalation",
      "type": "n8n-nodes-base.emailSend",
      "typeVersion": 2.1,
      "position": [
        1680,
        400
      ],
      "credentials": {
        "smtp": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "method": "POST",
        "url": "https://graph.facebook.com/v19.0/me/messages",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpQueryAuth",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={\n  \"recipient\": {\n    \"id\": \"{{ $json.senderId }}\"\n  },\n  \"message\": {\n    \"text\": \"{{ $json.replyText }}\"\n  },\n  \"messaging_type\": \"RESPONSE\"\n}",
        "options": {
          "timeout": 10000
        }
      },
      "id": "b1000000-0000-4000-8000-000000000012",
      "name": "Send Reply (RSVP)",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        1920,
        200
      ],
      "credentials": {
        "httpQueryAuth": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "method": "POST",
        "url": "https://graph.facebook.com/v19.0/me/messages",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpQueryAuth",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={\n  \"recipient\": {\n    \"id\": \"{{ $json.senderId }}\"\n  },\n  \"message\": {\n    \"text\": \"{{ $json.replyText }}\"\n  },\n  \"messaging_type\": \"RESPONSE\"\n}",
        "options": {
          "timeout": 10000
        }
      },
      "id": "b1000000-0000-4000-8000-000000000013",
      "name": "Send Reply (Escalate)",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        1920,
        400
      ],
      "credentials": {
        "httpQueryAuth": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "documentId": {
          "__rl": true,
          "value": "YOUR_GOOGLE_SHEET_ID",
          "mode": "id"
        },
        "sheetName": {
          "__rl": true,
          "value": "Sheet1",
          "mode": "name"
        },
        "columns": {
          "mappingMode": "defineBelow",
          "value": {
            "timestamp": "={{ $json.timestamp }}",
            "channel": "={{ $json.channel }}",
            "sender_id": "={{ $json.senderId }}",
            "incoming_message": "={{ $json.incomingText }}",
            "reply": "={{ $json.replyText }}",
            "action": "={{ $json.action }}",
            "event_name": "={{ $json.eventName }}",
            "escalate_reason": "={{ $json.escalateReason }}"
          },
          "matchingColumns": [],
          "schema": [
            {
              "id": "timestamp",
              "displayName": "timestamp",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "channel",
              "displayName": "channel",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "sender_id",
              "displayName": "sender_id",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "incoming_message",
              "displayName": "incoming_message",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": false
            },
            {
              "id": "reply",
              "displayName": "reply",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": false
            },
            {
              "id": "action",
              "displayName": "action",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "event_name",
              "displayName": "event_name",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": false
            },
            {
              "id": "escalate_reason",
              "displayName": "escalate_reason",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": false
            }
          ]
        },
        "options": {}
      },
      "id": "b1000000-0000-4000-8000-000000000014",
      "name": "Log to Google Sheet (RSVP)",
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4.5,
      "position": [
        2160,
        200
      ],
      "credentials": {
        "googleSheetsOAuth2Api": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "documentId": {
          "__rl": true,
          "value": "YOUR_GOOGLE_SHEET_ID",
          "mode": "id"
        },
        "sheetName": {
          "__rl": true,
          "value": "Sheet1",
          "mode": "name"
        },
        "columns": {
          "mappingMode": "defineBelow",
          "value": {
            "timestamp": "={{ $json.timestamp }}",
            "channel": "={{ $json.channel }}",
            "sender_id": "={{ $json.senderId }}",
            "incoming_message": "={{ $json.incomingText }}",
            "reply": "={{ $json.replyText }}",
            "action": "={{ $json.action }}",
            "event_name": "={{ $json.eventName }}",
            "escalate_reason": "={{ $json.escalateReason }}"
          },
          "matchingColumns": [],
          "schema": [
            {
              "id": "timestamp",
              "displayName": "timestamp",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "channel",
              "displayName": "channel",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "sender_id",
              "displayName": "sender_id",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "incoming_message",
              "displayName": "incoming_message",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": false
            },
            {
              "id": "reply",
              "displayName": "reply",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": false
            },
            {
              "id": "action",
              "displayName": "action",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "event_name",
              "displayName": "event_name",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": false
            },
            {
              "id": "escalate_reason",
              "displayName": "escalate_reason",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": false
            }
          ]
        },
        "options": {}
      },
      "id": "b1000000-0000-4000-8000-000000000015",
      "name": "Log to Google Sheet (Escalate)",
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4.5,
      "position": [
        2160,
        400
      ],
      "credentials": {
        "googleSheetsOAuth2Api": {
          "name": "<your credential>"
        }
      }
    }
  ],
  "connections": {
    "Webhook": {
      "main": [
        [
          {
            "node": "Extract Message",
            "type": "main",
            "index": 0
          },
          {
            "node": "Respond OK",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Extract Message": {
      "main": [
        [
          {
            "node": "If Has Message",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "If Has Message": {
      "main": [
        [
          {
            "node": "Claude API",
            "type": "main",
            "index": 0
          }
        ],
        []
      ]
    },
    "Claude API": {
      "main": [
        [
          {
            "node": "Parse Response",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Parse Response": {
      "main": [
        [
          {
            "node": "Route Action",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Route Action": {
      "main": [
        [
          {
            "node": "Google Calendar RSVP",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Email Escalation",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Send Reply",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Send Reply",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Google Calendar RSVP": {
      "main": [
        [
          {
            "node": "Send Reply (RSVP)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Send Reply (RSVP)": {
      "main": [
        [
          {
            "node": "Log to Google Sheet (RSVP)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Email Escalation": {
      "main": [
        [
          {
            "node": "Send Reply (Escalate)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Send Reply (Escalate)": {
      "main": [
        [
          {
            "node": "Log to Google Sheet (Escalate)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Send Reply": {
      "main": [
        [
          {
            "node": "Log to Google Sheet",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "settings": {
    "executionOrder": "v1"
  },
  "staticData": null,
  "tags": [
    {
      "name": "Meta",
      "createdAt": "2026-02-16T00:00:00.000Z",
      "updatedAt": "2026-02-16T00:00:00.000Z"
    },
    {
      "name": "Messenger",
      "createdAt": "2026-02-16T00:00:00.000Z",
      "updatedAt": "2026-02-16T00:00:00.000Z"
    },
    {
      "name": "Instagram",
      "createdAt": "2026-02-16T00:00:00.000Z",
      "updatedAt": "2026-02-16T00:00:00.000Z"
    }
  ],
  "triggerCount": 1,
  "updatedAt": "2026-02-16T00:00:00.000Z",
  "versionId": "1"
}