{
  "name": "Route new and returning enquiries in Appdor with duplicate checks",
  "active": false,
  "settings": {
    "executionOrder": "v1",
    "saveDataSuccessExecution": "none",
    "saveDataErrorExecution": "none",
    "saveManualExecutions": false
  },
  "nodes": [
    {
      "id": "start-here",
      "name": "Start here",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        -20,
        -650
      ],
      "parameters": {
        "content": "## Route new and returning enquiries in Appdor\n**Free template \u00b7 supplied inactive \u00b7 no credentials included**\nAn n8n form validates and normalizes enquiries, searches Appdor by email, creates a new record or updates its latest enquiry with optimistic version checking, and routes a metadata-only notification by category/urgency.\n\n### 1. Create an Enquiries table\nExact columns: **Name** (text), **Email** (email), **LastMessage** (long text), **EnquiryType**, **Priority**, **Queue**, **Source**, **Status** (text), **LastReceivedAt** (date with time).\nStatus starts as New and is preserved on repeat enquiries. LastMessage is replaced; this is a latest-enquiry record, not message history.\n\n### 2. Configure\nEdit the fixed configuration at the top of **Configure and validate enquiry**. Set your HTTPS origin, table UUID, sender and four team addresses. Form users never supply destinations. Add one HTTP Bearer Auth credential with Appdor **data:read + data:write** to both HTTP nodes, and SMTP credentials to both email nodes.\n\nAppdor is commercial software. Appdor, n8n and mail hosting costs depend on your plans. Update the form privacy notice and spam/access controls before activation.\nGuide: https://appdor.com/grow/automation-guide.html",
        "width": 1050,
        "height": 540,
        "color": 1
      }
    },
    {
      "id": "duplicate-checks-and-limits",
      "name": "Duplicate checks and limits",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        1080,
        -520
      ],
      "parameters": {
        "content": "## 3. Lookup \u2192 decide \u2192 write\nThe query runs a case-insensitive trimmed-email comparison inside the database, selecting Email and at most two matching records.\n**0 matches:** create with Status = New.\n**1 match:** update latest enquiry using its returned version; preserve Status and other fields.\n**2 matches:** stop for human duplicate review; never choose a record arbitrarily.\n\nA read or query error is never treated as an empty result. The API credential must see every relevant row and field.\n\n**Concurrency limit:** lookup + create is not atomic. Simultaneous submissions or other writers can still create duplicates. Serialize the whole lookup/write sequence across all writers, or add database uniqueness before relying on strict deduplication. The API idempotency cache is limited and not a uniqueness guarantee.",
        "width": 800,
        "height": 410,
        "color": 4
      }
    },
    {
      "id": "routing-and-recovery",
      "name": "Routing and recovery",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        1900,
        -610
      ],
      "parameters": {
        "content": "## 4. Route only after a confirmed save\nNormal priority \u2192 fixed Sales, Support or Billing inbox.\nUrgent priority \u2192 fixed escalation inbox.\nNotifications contain record ID, category, priority and queue only; no name, email or message.\n\n### Recover safely\n**Lookup fails:** correct read scope/table/schema/connectivity, then resubmit. No write or email has occurred.\n**Save fails or times out:** inspect Appdor first. An uncertain response may still have saved. Re-query by email before retrying. For a version conflict, fetch the current record and review before rerunning; no blind automatic retry.\n**SMTP fails:** the record is already saved. Retry only that email step after fixing SMTP, or send the internal notification manually. If delivery is uncertain, check the mailbox first; SMTP cannot guarantee exactly-once delivery.\n\nErrors use explicit Stop And Error branches. Form success appears only after SMTP completes. Successful/error execution payload retention is disabled; use synthetic data while testing, and do not paste real submissions or credentials into logs/screenshots.",
        "width": 970,
        "height": 510,
        "color": 4
      }
    },
    {
      "id": "verify-before-activation",
      "name": "Verify before activation",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        -20,
        420
      ],
      "parameters": {
        "content": "## Test with synthetic data\n1. New normal Sales enquiry \u2192 one New record and one Sales notification.\n2. Same email with different case/spaces \u2192 update that record, preserve Status, no second record.\n3. Urgent Support enquiry \u2192 escalation inbox, record marked Support/Urgent.\n4. Two existing email matches \u2192 explicit failure, no write or email.\n5. Invalid email/category/oversized message \u2192 reject before lookup.\n6. Invalid API credential, stale version and unavailable SMTP \u2192 correct failure branch; no false success.\n\nTemplate logic and import are tested separately from your live services. Verify your own Appdor/SMTP setup, permissions and expected concurrency before publishing the form. This download is not an approved n8n library listing.",
        "width": 1120,
        "height": 330,
        "color": 4
      }
    },
    {
      "id": "appdor-form",
      "name": "Enquiry form",
      "type": "n8n-nodes-base.formTrigger",
      "typeVersion": 2,
      "position": [
        0,
        0
      ],
      "webhookId": "a32d56a1-4806-4bdd-88cd-307cd4e9f5a9",
      "parameters": {
        "formTitle": "Send an enquiry",
        "formDescription": "Your enquiry will be stored in our Appdor workspace and routed to our team. Only record metadata is emailed internally. Do not submit passwords or sensitive records. The workflow owner must add its intake privacy notice before publishing.",
        "formFields": {
          "values": [
            {
              "fieldLabel": "Name",
              "requiredField": true
            },
            {
              "fieldLabel": "Email",
              "fieldType": "email",
              "requiredField": true
            },
            {
              "fieldLabel": "Message",
              "fieldType": "textarea",
              "requiredField": true
            },
            {
              "fieldLabel": "Enquiry type",
              "fieldType": "dropdown",
              "fieldOptions": {
                "values": [
                  {
                    "option": "Sales"
                  },
                  {
                    "option": "Support"
                  },
                  {
                    "option": "Billing"
                  }
                ]
              },
              "requiredField": true
            },
            {
              "fieldLabel": "Priority",
              "fieldType": "dropdown",
              "fieldOptions": {
                "values": [
                  {
                    "option": "Normal"
                  },
                  {
                    "option": "Urgent"
                  }
                ]
              },
              "requiredField": true
            }
          ]
        },
        "responseMode": "lastNode",
        "options": {
          "respondWithOptions": {
            "values": {
              "formSubmittedText": "Your enquiry has been saved and our team notified."
            }
          }
        }
      }
    },
    {
      "id": "configure-and-validate-enquiry",
      "name": "Configure and validate enquiry",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        260,
        0
      ],
      "parameters": {
        "jsCode": "// Edit only this fixed configuration. Never place API/SMTP secrets here.\nconst config = {\n  origin: 'https://YOUR-APPDOR-HOST.example',\n  tableId: 'YOUR-TABLE-UUID',\n  fromEmail: 'notifications@YOUR-DOMAIN.example',\n  urgentEmail: 'urgent@YOUR-DOMAIN.example',\n  queues: { Sales: 'sales@YOUR-DOMAIN.example', Support: 'support@YOUR-DOMAIN.example', Billing: 'billing@YOUR-DOMAIN.example' },\n};\nconst input = $input.first().json;\nconst field = (key, max) => {\n  const value = input[key];\n  if (typeof value !== 'string' || !value.trim() || value.length > max) throw new Error('Invalid ' + key);\n  return value.trim();\n};\nconst name = field('Name', 200);\nconst email = field('Email', 254).toLowerCase();\nconst message = field('Message', 5000);\nconst enquiryType = field('Enquiry type', 20);\nconst priority = field('Priority', 10);\nconst emailPattern = /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/;\nif (!emailPattern.test(email)) throw new Error('Invalid Email');\nif (!['Sales', 'Support', 'Billing'].includes(enquiryType)) throw new Error('Invalid Enquiry type');\nif (!['Normal', 'Urgent'].includes(priority)) throw new Error('Invalid Priority');\nconst origin = config.origin.replace(/\\/$/, '');\nif (!/^https:\\/\\/[a-z0-9.-]+(?::[0-9]+)?$/i.test(origin) || /\\.example(?::[0-9]+)?$/i.test(origin)) throw new Error('Configure your real HTTPS Appdor origin');\nif (!/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(config.tableId)) throw new Error('Configure the Appdor table UUID');\nif (![config.fromEmail, config.urgentEmail, ...Object.values(config.queues)].every(v => typeof v === 'string' && emailPattern.test(v) && !/\\.example$/i.test(v))) throw new Error('Configure your fixed sender and team email addresses');\nreturn [{ json: {\n  config: { ...config, origin },\n  fields: { Name: name, Email: email, LastMessage: message, EnquiryType: enquiryType, Priority: priority, Queue: enquiryType, Source: 'n8n enquiry form', LastReceivedAt: new Date().toISOString() },\n  query: { filter: 'TRIM({Email}) = ' + JSON.stringify(email), first: 2, select: ['Email'] },\n} }];"
      }
    },
    {
      "id": "lookup-matching-enquiry",
      "name": "Lookup matching enquiry",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        520,
        0
      ],
      "parameters": {
        "method": "POST",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpBearerAuth",
        "sendBody": true,
        "specifyBody": "json",
        "options": {
          "timeout": 15000
        },
        "url": "={{ $json.config.origin + '/api/v1/tables/' + $json.config.tableId + '/query' }}",
        "jsonBody": "={{ JSON.stringify($json.query) }}"
      },
      "retryOnFail": false,
      "onError": "continueErrorOutput"
    },
    {
      "id": "plan-record-write",
      "name": "Plan record write",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        790,
        0
      ],
      "parameters": {
        "jsCode": "const response = $input.first().json;\nconst intake = $('Configure and validate enquiry').first().json;\nif (response.ok !== true || !Array.isArray(response.data)) throw new Error('Lookup returned an invalid response; no write or notification attempted. Fix the API before retrying.');\nif (response.data.length > 1) throw new Error('Multiple matching enquiries: review and merge duplicates in Appdor before resubmitting. No write or notification attempted.');\nconst record = response.data[0];\nif (record && (!/^[0-9a-f-]{36}$/i.test(record.id || '') || typeof record.version !== 'string' || !record.version || typeof record.fields?.Email !== 'string' || record.fields.Email.trim().toLowerCase() !== intake.fields.Email)) throw new Error('Lookup returned an unverifiable record or version; no write attempted. Check permissions and the query response.');\nconst body = { fields: { ...intake.fields } };\nif (record) body.version = record.version;\nelse body.fields.Status = 'New';\nreturn [{ json: {\n  method: record ? 'PATCH' : 'POST',\n  recordUrl: intake.config.origin + '/api/v1/tables/' + intake.config.tableId + '/records' + (record ? '/' + record.id : ''),\n  expectedId: record ? record.id : null,\n  body,\n} }];"
      }
    },
    {
      "id": "create-or-update-enquiry",
      "name": "Create or update enquiry",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        1060,
        0
      ],
      "parameters": {
        "method": "={{ $json.method }}",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpBearerAuth",
        "sendBody": true,
        "specifyBody": "json",
        "options": {
          "timeout": 15000
        },
        "url": "={{ $json.recordUrl }}",
        "jsonBody": "={{ JSON.stringify($json.body) }}",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "Idempotency-Key",
              "value": "=appdor-enquiry-v2-{{$execution.id}}"
            }
          ]
        }
      },
      "retryOnFail": false,
      "onError": "continueErrorOutput"
    },
    {
      "id": "confirm-saved-enquiry",
      "name": "Confirm saved enquiry",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1330,
        0
      ],
      "parameters": {
        "jsCode": "const response = $input.first().json;\nconst plan = $('Plan record write').first().json;\nconst intake = $('Configure and validate enquiry').first().json;\nconst id = response.data?.id;\nif (response.ok !== true || typeof id !== 'string' || !/^[0-9a-f-]{36}$/i.test(id) || (plan.expectedId && id !== plan.expectedId)) throw new Error('Save response could not be verified. Inspect Appdor and re-query by email before retrying; no notification attempted.');\nreturn [{ json: { recordId: id, operation: plan.expectedId ? 'updated' : 'created', enquiryType: intake.fields.EnquiryType, priority: intake.fields.Priority, queue: intake.fields.Queue } }];"
      }
    },
    {
      "id": "urgent-enquiry",
      "name": "Urgent enquiry?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        1590,
        0
      ],
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict",
            "version": 2
          },
          "conditions": [
            {
              "id": "urgent",
              "leftValue": "={{ $json.priority }}",
              "rightValue": "Urgent",
              "operator": {
                "type": "string",
                "operation": "equals"
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      }
    },
    {
      "id": "notify-urgent-queue",
      "name": "Notify urgent queue",
      "type": "n8n-nodes-base.emailSend",
      "typeVersion": 2.1,
      "position": [
        1840,
        -80
      ],
      "parameters": {
        "fromEmail": "={{ $('Configure and validate enquiry').first().json.config.fromEmail }}",
        "toEmail": "={{ $('Configure and validate enquiry').first().json.config.urgentEmail }}",
        "subject": "={{ '[' + $json.priority + '] Appdor enquiry ' + $json.operation + ' \u2014 ' + $json.enquiryType }}",
        "emailFormat": "text",
        "text": "={{ 'An enquiry was ' + $json.operation + ' in Appdor.\\nRecord ID: ' + $json.recordId + '\\nCategory: ' + $json.enquiryType + '\\nPriority: ' + $json.priority + '\\nQueue: ' + $json.queue + '\\nOpen your configured Enquiries table to review it.' }}",
        "options": {
          "appendAttribution": false
        }
      },
      "retryOnFail": false,
      "onError": "continueErrorOutput"
    },
    {
      "id": "notify-assigned-queue",
      "name": "Notify assigned queue",
      "type": "n8n-nodes-base.emailSend",
      "typeVersion": 2.1,
      "position": [
        1840,
        140
      ],
      "parameters": {
        "fromEmail": "={{ $('Configure and validate enquiry').first().json.config.fromEmail }}",
        "toEmail": "={{ $('Configure and validate enquiry').first().json.config.queues[$json.enquiryType] }}",
        "subject": "={{ '[' + $json.priority + '] Appdor enquiry ' + $json.operation + ' \u2014 ' + $json.enquiryType }}",
        "emailFormat": "text",
        "text": "={{ 'An enquiry was ' + $json.operation + ' in Appdor.\\nRecord ID: ' + $json.recordId + '\\nCategory: ' + $json.enquiryType + '\\nPriority: ' + $json.priority + '\\nQueue: ' + $json.queue + '\\nOpen your configured Enquiries table to review it.' }}",
        "options": {
          "appendAttribution": false
        }
      },
      "retryOnFail": false,
      "onError": "continueErrorOutput"
    },
    {
      "id": "confirm-receipt",
      "name": "Confirm receipt",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        2130,
        0
      ],
      "parameters": {
        "jsCode": "return [{ json: { received: true, recordId: $('Confirm saved enquiry').first().json.recordId } }];"
      }
    },
    {
      "id": "stop-lookup-failure",
      "name": "Stop lookup failure",
      "type": "n8n-nodes-base.stopAndError",
      "typeVersion": 1,
      "position": [
        520,
        240
      ],
      "parameters": {
        "errorType": "errorMessage",
        "errorMessage": "Lookup failed. No record write or notification was attempted. Check the fixed origin, table, schema, data:read scope and connectivity, then retry with synthetic data."
      }
    },
    {
      "id": "stop-save-failure",
      "name": "Stop save failure",
      "type": "n8n-nodes-base.stopAndError",
      "typeVersion": 1,
      "position": [
        1060,
        240
      ],
      "parameters": {
        "errorType": "errorMessage",
        "errorMessage": "Save failed or its outcome is uncertain. No notification was attempted. Inspect Appdor and re-query the normalized email before retrying. For a version conflict, fetch and review the latest record. Do not blindly replay a create."
      }
    },
    {
      "id": "stop-notification-failure",
      "name": "Stop notification failure",
      "type": "n8n-nodes-base.stopAndError",
      "typeVersion": 1,
      "position": [
        2130,
        260
      ],
      "parameters": {
        "errorType": "errorMessage",
        "errorMessage": "The Appdor record was saved, but email delivery failed or is uncertain. Check SMTP and the recipient mailbox before retrying ONLY the notification step, or send an internal notification manually. Do not rerun the whole form merely to retry email."
      }
    }
  ],
  "connections": {
    "Enquiry form": {
      "main": [
        [
          {
            "node": "Configure and validate enquiry",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Configure and validate enquiry": {
      "main": [
        [
          {
            "node": "Lookup matching enquiry",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Lookup matching enquiry": {
      "main": [
        [
          {
            "node": "Plan record write",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Stop lookup failure",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Plan record write": {
      "main": [
        [
          {
            "node": "Create or update enquiry",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Create or update enquiry": {
      "main": [
        [
          {
            "node": "Confirm saved enquiry",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Stop save failure",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Confirm saved enquiry": {
      "main": [
        [
          {
            "node": "Urgent enquiry?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Urgent enquiry?": {
      "main": [
        [
          {
            "node": "Notify urgent queue",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Notify assigned queue",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Notify urgent queue": {
      "main": [
        [
          {
            "node": "Confirm receipt",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Stop notification failure",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Notify assigned queue": {
      "main": [
        [
          {
            "node": "Confirm receipt",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Stop notification failure",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "pinData": {},
  "tags": [],
  "id": "AppdorEnquiryTemplate"
}
