{
  "name": "Send a rescheduling email after a missed appointment",
  "tags": [],
  "nodes": [
    {
      "id": "overview-note",
      "name": "Workflow guide",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -520,
        -320
      ],
      "parameters": {
        "color": 4,
        "width": 520,
        "height": 580,
        "content": "## Send a rescheduling email after a missed appointment\n\nThis workflow receives a missed-appointment webhook, validates the event, blocks ineligible contacts, sends one rescheduling email through Gmail, and returns a JSON acknowledgement.\n\n### Who it is for\n\nLocal businesses, agencies, and operators who receive appointment status updates from a CRM or scheduling system.\n\n### Setup\n\n1. Open **Configure recovery message** and set the business name, booking URL, and subject.\n2. Connect a customer-owned Gmail credential to **Send rescheduling email**.\n3. Send the example payload below to the test webhook.\n4. Confirm that non-missed appointments and contacts with `transactional_contact_allowed: false` do not send.\n5. Activate only after the business approves the message and data flow.\n\nThe imported workflow is inactive. Use a contact you control for testing."
      },
      "typeVersion": 1
    },
    {
      "id": "payload-note",
      "name": "Test payload",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -520,
        300
      ],
      "parameters": {
        "color": 7,
        "width": 440,
        "height": 430,
        "content": "## Example webhook payload\n\n```json\n{\n  \"event_id\": \"evt_test_001\",\n  \"contact\": {\n    \"name\": \"Test Customer\",\n    \"email\": \"operator-controlled@example.com\",\n    \"transactional_contact_allowed\": true\n  },\n  \"appointment\": {\n    \"id\": \"apt_test_001\",\n    \"status\": \"missed\"\n  }\n}\n```\n\nUse a stable, unique `event_id`. For production, add a Data Store lookup if your email provider does not support idempotency."
      },
      "typeVersion": 1
    },
    {
      "id": "missed-webhook",
      "name": "Receive missed appointment",
      "type": "n8n-nodes-base.webhook",
      "position": [
        40,
        0
      ],
      "parameters": {
        "path": "missed-appointment-recovery",
        "options": {},
        "httpMethod": "POST",
        "responseMode": "responseNode"
      },
      "typeVersion": 2
    },
    {
      "id": "configuration",
      "name": "Configure recovery message",
      "type": "n8n-nodes-base.set",
      "position": [
        280,
        0
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "business-name",
              "name": "business_name",
              "type": "string",
              "value": "Your Business"
            },
            {
              "id": "booking-url",
              "name": "booking_url",
              "type": "string",
              "value": "https://example.com/book"
            },
            {
              "id": "email-subject",
              "name": "email_subject",
              "type": "string",
              "value": "Would you like to reschedule?"
            }
          ]
        },
        "includeOtherFields": true
      },
      "typeVersion": 3.4
    },
    {
      "id": "prepare",
      "name": "Validate event and prepare email",
      "type": "n8n-nodes-base.code",
      "position": [
        520,
        0
      ],
      "parameters": {
        "jsCode": "const p = $json.body || $json;\nconst required = [p.event_id, p.contact?.email, p.appointment?.id];\nif (required.some((value) => !value)) {\n  throw new Error('Missing event_id, contact.email, or appointment.id');\n}\n\nconst status = String(p.appointment.status || '').toLowerCase();\nconst blocked = p.contact.transactional_contact_allowed === false || status !== 'missed';\nconst greeting = p.contact.name ? `Hi ${p.contact.name},` : 'Hi there,';\nconst message = `${greeting}\\n\\nWe missed you at your appointment with ${$json.business_name}. You can choose a new time here: ${$json.booking_url}\\n\\nReply to this email if you need help rescheduling.`;\n\nreturn [{\n  json: {\n    event_id: p.event_id,\n    appointment_id: p.appointment.id,\n    contact_email: p.contact.email,\n    blocked,\n    email_subject: $json.email_subject,\n    message\n  }\n}];"
      },
      "typeVersion": 2
    },
    {
      "id": "allowed",
      "name": "Is recovery allowed?",
      "type": "n8n-nodes-base.if",
      "position": [
        760,
        0
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 2,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "allowed-condition",
              "operator": {
                "type": "boolean",
                "operation": "equals"
              },
              "leftValue": "={{ $json.blocked }}",
              "rightValue": false
            }
          ]
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "send",
      "name": "Send rescheduling email",
      "type": "n8n-nodes-base.gmail",
      "onError": "continueErrorOutput",
      "position": [
        1000,
        -100
      ],
      "parameters": {
        "sendTo": "={{ $json.contact_email }}",
        "message": "={{ $json.message }}",
        "options": {},
        "subject": "={{ $json.email_subject }}",
        "emailType": "text"
      },
      "typeVersion": 2.1
    },
    {
      "id": "response",
      "name": "Return webhook acknowledgement",
      "type": "n8n-nodes-base.respondToWebhook",
      "position": [
        1240,
        0
      ],
      "parameters": {
        "options": {},
        "respondWith": "json",
        "responseBody": "={{ { accepted: true, eventId: $json.event_id, blocked: $json.blocked || false } }}"
      },
      "typeVersion": 1.4
    },
    {
      "id": "safeguards-note",
      "name": "Production safeguards",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        760,
        260
      ],
      "parameters": {
        "color": 7,
        "width": 420,
        "height": 300,
        "content": "## Production safeguards\n\n- Re-check appointment status before any delayed send.\n- Use a Data Store or provider idempotency key to suppress duplicate event IDs.\n- Keep the message transactional; do not add unrelated promotions.\n- Connect credentials owned by the end customer.\n- Review failed executions and provide a human support route."
      },
      "typeVersion": 1
    }
  ],
  "active": false,
  "settings": {
    "executionOrder": "v1"
  },
  "connections": {
    "Is recovery allowed?": {
      "main": [
        [
          {
            "node": "Send rescheduling email",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Return webhook acknowledgement",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Send rescheduling email": {
      "main": [
        [
          {
            "node": "Return webhook acknowledgement",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Return webhook acknowledgement",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Configure recovery message": {
      "main": [
        [
          {
            "node": "Validate event and prepare email",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Receive missed appointment": {
      "main": [
        [
          {
            "node": "Configure recovery message",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Validate event and prepare email": {
      "main": [
        [
          {
            "node": "Is recovery allowed?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}