{
  "name": "Win-back text (Insurance Agents)",
  "nodes": [
    {
      "id": "8b3f46c2-daf4-5633-94e9-5ee8080377e5",
      "name": "Every morning",
      "type": "n8n-nodes-base.scheduleTrigger",
      "typeVersion": 1.2,
      "position": [
        0,
        0
      ],
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "days",
              "triggerAtHour": 10
            }
          ]
        }
      }
    },
    {
      "id": "c49cc977-116d-5382-8cd8-ff8c9cdc4646",
      "name": "Incoming text",
      "type": "n8n-nodes-base.webhook",
      "typeVersion": 2,
      "position": [
        0,
        300
      ],
      "parameters": {
        "httpMethod": "POST",
        "path": "win-back-sms",
        "responseMode": "responseNode",
        "options": {}
      },
      "webhookId": "56a4a699-dc7f-51bf-b482-1b7d7b671d07"
    },
    {
      "id": "10e51077-dc33-5f98-91cd-9b74b0b636e6",
      "name": "Config (edit me)",
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        220,
        140
      ],
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "031bc5da-69dc-5c85-a59f-90ec4a993566",
              "name": "business_name",
              "value": "YOUR BUSINESS NAME",
              "type": "string"
            },
            {
              "id": "b1911bd3-a8bb-5ab4-a484-13a80d9c3b4c",
              "name": "booking_link",
              "value": "https://YOUR-BOOKING-LINK",
              "type": "string"
            },
            {
              "id": "34115c75-5d4a-563d-b783-1e2d1f8cac53",
              "name": "hours",
              "value": "Mon-Fri 8am-6pm",
              "type": "string"
            },
            {
              "id": "857ca168-ea99-5ab2-aa51-3787f94e2884",
              "name": "owner_email",
              "value": "you@example.com",
              "type": "string"
            },
            {
              "id": "a91afdb3-1f37-51d2-a0b9-b052ab9df6f5",
              "name": "owner_cell",
              "value": "+15555550100",
              "type": "string"
            },
            {
              "id": "3c65a52c-42f3-5a53-8819-7bcfd5a2a918",
              "name": "twilio_number",
              "value": "+15555550199",
              "type": "string"
            },
            {
              "id": "aeebeac6-4a39-5a80-ab07-1f12b0abce1a",
              "name": "twilio_auth_token",
              "value": "PASTE_YOUR_TWILIO_AUTH_TOKEN",
              "type": "string"
            },
            {
              "id": "1dec07c8-7e08-5945-879d-bf82e715dc24",
              "name": "twilio_sms_webhook_url",
              "value": "https://YOUR-N8N-URL/webhook/win-back-sms",
              "type": "string"
            },
            {
              "id": "bb3a12d8-4467-52d7-a021-98d5e7ff491e",
              "name": "clients_sheet_id",
              "value": "YOUR_GOOGLE_SHEET_ID",
              "type": "string"
            },
            {
              "id": "bcb00f47-b935-563a-9f7d-7a38dc3fc4ad",
              "name": "winback_days",
              "value": "4",
              "type": "string"
            },
            {
              "id": "7db21cae-249d-57fb-97f4-33a14d4ebee2",
              "name": "daily_cap",
              "value": "25",
              "type": "string"
            }
          ]
        },
        "includeOtherFields": true,
        "options": {}
      }
    },
    {
      "id": "80e8a6ea-b2cb-53df-a90b-c0594b9cf03d",
      "name": "Scheduled run or text?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2,
      "position": [
        440,
        140
      ],
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": false,
            "leftValue": "",
            "typeValidation": "loose"
          },
          "conditions": [
            {
              "id": "92ff6742-5b1d-592a-83c2-ef95e70a5a69",
              "leftValue": "={{ ($json.body || {}).MessageSid }}",
              "rightValue": "",
              "operator": {
                "type": "string",
                "operation": "empty",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      }
    },
    {
      "id": "53c9bd02-53e3-5e78-b2f8-3c076d24aee3",
      "name": "Read your list",
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4.5,
      "position": [
        660,
        0
      ],
      "parameters": {
        "documentId": {
          "__rl": true,
          "value": "={{ $('Config (edit me)').first().json.clients_sheet_id }}",
          "mode": "id"
        },
        "sheetName": {
          "__rl": true,
          "value": "Quotes",
          "mode": "name"
        },
        "options": {}
      },
      "credentials": {
        "googleSheetsOAuth2Api": {
          "id": "REPLACE_SHEETS_CREDENTIAL",
          "name": "Google Sheets account (placeholder)"
        }
      }
    },
    {
      "id": "b5ba0b3a-8249-5315-b261-70738836759d",
      "name": "Due for a nudge?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2,
      "position": [
        880,
        0
      ],
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "loose"
          },
          "conditions": [
            {
              "id": "e0653686-3282-544e-8510-30ccef07e49d",
              "leftValue": "={{ !!$json.phone && !String($json.opted_out || '').trim() && DateTime.fromISO(String($json.last_visit || '')).isValid && $now.diff(DateTime.fromISO(String($json.last_visit)), 'days').days >= Number($('Config (edit me)').first().json.winback_days) && (!String($json.last_texted || '').trim() || DateTime.fromISO(String($json.last_texted)) < DateTime.fromISO(String($json.last_visit))) }}",
              "rightValue": "",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      }
    },
    {
      "id": "e5e57a13-89c9-51d1-a9b7-65d10dbd7d27",
      "name": "Daily cap",
      "type": "n8n-nodes-base.limit",
      "typeVersion": 1,
      "position": [
        1100,
        0
      ],
      "parameters": {
        "maxItems": "={{ Number($('Config (edit me)').first().json.daily_cap) || 25 }}"
      }
    },
    {
      "id": "6774e071-11d2-5043-a0dd-7d0e566ad4ef",
      "name": "Send the win-back text",
      "type": "n8n-nodes-base.twilio",
      "typeVersion": 1,
      "position": [
        1320,
        0
      ],
      "parameters": {
        "from": "={{ $('Config (edit me)').first().json.twilio_number }}",
        "to": "={{ $json.phone }}",
        "message": "=Hi {{ String($json.name || '').split(' ')[0] || 'there' }}, it's {{ $('Config (edit me)').first().json.business_name }}. Just checking in on the quote we sent. Any questions for the agent? Pick a time to talk: {{ $('Config (edit me)').first().json.booking_link }} Reply STOP to opt out.",
        "options": {}
      },
      "credentials": {
        "twilioApi": {
          "id": "REPLACE_TWILIO_CREDENTIAL",
          "name": "Twilio account (placeholder)"
        }
      }
    },
    {
      "id": "87505a1c-239e-545e-9247-11b7d6ee6521",
      "name": "Mark as texted",
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4.5,
      "position": [
        1540,
        0
      ],
      "parameters": {
        "operation": "update",
        "documentId": {
          "__rl": true,
          "value": "={{ $('Config (edit me)').first().json.clients_sheet_id }}",
          "mode": "id"
        },
        "sheetName": {
          "__rl": true,
          "value": "Quotes",
          "mode": "name"
        },
        "columns": {
          "mappingMode": "defineBelow",
          "value": {
            "phone": "={{ $('Daily cap').item.json.phone }}",
            "last_texted": "={{ $now.toFormat('yyyy-MM-dd') }}"
          },
          "matchingColumns": [
            "phone"
          ],
          "schema": [
            {
              "id": "phone",
              "displayName": "phone",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "last_texted",
              "displayName": "last_texted",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            }
          ]
        },
        "options": {}
      },
      "credentials": {
        "googleSheetsOAuth2Api": {
          "id": "REPLACE_SHEETS_CREDENTIAL",
          "name": "Google Sheets account (placeholder)"
        }
      }
    },
    {
      "id": "b99a475c-af30-5b1b-b259-9de1a2462181",
      "name": "Email you a summary",
      "type": "n8n-nodes-base.gmail",
      "typeVersion": 2.1,
      "position": [
        1760,
        0
      ],
      "parameters": {
        "sendTo": "={{ $('Config (edit me)').first().json.owner_email }}",
        "subject": "=Win-back texts sent today: {{ $('Daily cap').all().length }}",
        "emailType": "text",
        "message": "=Texted {{ $('Daily cap').all().length }} people from your list today. Replies come to you by email; anyone who replies STOP is opted out by Twilio automatically (also put an x in their opted_out cell).",
        "options": {}
      },
      "credentials": {
        "gmailOAuth2": {
          "id": "REPLACE_GMAIL_CREDENTIAL",
          "name": "Gmail account (placeholder)"
        }
      },
      "onError": "continueRegularOutput",
      "executeOnce": true
    },
    {
      "id": "55bcd493-3570-5126-860f-3300f5726cbd",
      "name": "Check Twilio signature",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        660,
        460
      ],
      "parameters": {
        "mode": "runOnceForEachItem",
        "jsCode": "// Is this request really from Twilio? Checks the X-Twilio-Signature header.\n// Twilio signs: the exact webhook URL + every POST field, sorted by name, as name+value,\n// using HMAC-SHA1 keyed with your auth token, base64-encoded.\n// n8n Cloud allows require('crypto'). Self-hosted: set NODE_FUNCTION_ALLOW_BUILTIN=crypto\n// (on the task runner too, if you use one).\nconst crypto = require('crypto');\nconst cfg = $('Config (edit me)').first().json;\nconst req = $json;\nconst params = req.body || {};\nconst url = String(cfg.twilio_sms_webhook_url || '').trim();\nconst data = url + Object.keys(params).sort().map(k => k + params[k]).join('');\nconst expected = crypto.createHmac('sha1', String(cfg.twilio_auth_token || '')).update(data, 'utf8').digest('base64');\nconst received = String((req.headers || {})['x-twilio-signature'] || '');\nconst a = Buffer.from(expected);\nconst b = Buffer.from(received);\nconst ok = url !== '' && received !== '' && a.length === b.length && crypto.timingSafeEqual(a, b);\nreturn { json: { ...req, signature_ok: ok } };"
      }
    },
    {
      "id": "f191641c-d082-5db0-92a3-350138c94b7a",
      "name": "Signature valid?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2,
      "position": [
        880,
        460
      ],
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "loose"
          },
          "conditions": [
            {
              "id": "a9b124c2-77ce-5b51-8e98-df69975d3fd2",
              "leftValue": "={{ $json.signature_ok }}",
              "rightValue": "",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      }
    },
    {
      "id": "dad544f0-10e8-525c-b7f9-c661f49c8137",
      "name": "Reject: not from Twilio (403)",
      "type": "n8n-nodes-base.respondToWebhook",
      "typeVersion": 1.1,
      "position": [
        1100,
        640
      ],
      "parameters": {
        "respondWith": "text",
        "responseBody": "Forbidden",
        "options": {
          "responseHeaders": {
            "entries": [
              {
                "name": "Content-Type",
                "value": "text/plain"
              }
            ]
          },
          "responseCode": 403
        }
      }
    },
    {
      "id": "b6dfa735-f828-5953-99cc-90f93c70b52c",
      "name": "Acknowledge Twilio (text)",
      "type": "n8n-nodes-base.respondToWebhook",
      "typeVersion": 1.1,
      "position": [
        1100,
        460
      ],
      "parameters": {
        "respondWith": "text",
        "responseBody": "<Response></Response>",
        "options": {
          "responseHeaders": {
            "entries": [
              {
                "name": "Content-Type",
                "value": "text/xml"
              }
            ]
          }
        }
      }
    },
    {
      "id": "17c7e9a9-877c-5feb-b06c-e1e6ab48d0e1",
      "name": "Says URGENT?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2,
      "position": [
        1320,
        460
      ],
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": false,
            "leftValue": "",
            "typeValidation": "loose"
          },
          "conditions": [
            {
              "id": "2b7ec0c5-d535-5b27-98c0-d0fbb67ec0ab",
              "leftValue": "={{ $('Incoming text').first().json.body.Body }}",
              "rightValue": "urgent",
              "operator": {
                "type": "string",
                "operation": "contains"
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      }
    },
    {
      "id": "866dcb8e-eeb6-5ddb-8cc1-bc240d697ef7",
      "name": "Text the owner (URGENT)",
      "type": "n8n-nodes-base.twilio",
      "typeVersion": 1,
      "position": [
        1540,
        280
      ],
      "parameters": {
        "from": "={{ $('Config (edit me)').first().json.twilio_number }}",
        "to": "={{ $('Config (edit me)').first().json.owner_cell }}",
        "message": "=URGENT text to {{ $('Config (edit me)').first().json.business_name }} from {{ $('Incoming text').first().json.body.From }}: \"{{ $('Incoming text').first().json.body.Body }}\" Call or text them back now.",
        "options": {}
      },
      "credentials": {
        "twilioApi": {
          "id": "REPLACE_TWILIO_CREDENTIAL",
          "name": "Twilio account (placeholder)"
        }
      }
    },
    {
      "id": "4e964ac5-7f9f-5e35-b67a-b31749ce84ae",
      "name": "Text the customer (URGENT ack)",
      "type": "n8n-nodes-base.twilio",
      "typeVersion": 1,
      "position": [
        1760,
        280
      ],
      "parameters": {
        "from": "={{ $('Config (edit me)').first().json.twilio_number }}",
        "to": "={{ $('Incoming text').first().json.body.From }}",
        "message": "=Got it, we've texted the agent's cell and someone will call you shortly. Coverage can't be started or changed by text. If anyone is hurt or in danger, call 911. - {{ $('Config (edit me)').first().json.business_name }}",
        "options": {}
      },
      "credentials": {
        "twilioApi": {
          "id": "REPLACE_TWILIO_CREDENTIAL",
          "name": "Twilio account (placeholder)"
        }
      }
    },
    {
      "id": "72362768-e100-5f68-989f-9183559cc60f",
      "name": "Email the owner (URGENT, optional)",
      "type": "n8n-nodes-base.gmail",
      "typeVersion": 2.1,
      "position": [
        1980,
        280
      ],
      "parameters": {
        "sendTo": "={{ $('Config (edit me)').first().json.owner_email }}",
        "subject": "=URGENT: text from {{ $('Incoming text').first().json.body.From }}",
        "emailType": "text",
        "message": "=They wrote: {{ $('Incoming text').first().json.body.Body }}\n\nYour cell was texted and they were told you'll get back to them right away.",
        "options": {}
      },
      "credentials": {
        "gmailOAuth2": {
          "id": "REPLACE_GMAIL_CREDENTIAL",
          "name": "Gmail account (placeholder)"
        }
      },
      "onError": "continueRegularOutput"
    },
    {
      "id": "30ab46f9-0f7f-533b-930b-e9378855269d",
      "name": "Forward the reply to you",
      "type": "n8n-nodes-base.gmail",
      "typeVersion": 2.1,
      "position": [
        1540,
        640
      ],
      "parameters": {
        "sendTo": "={{ $('Config (edit me)').first().json.owner_email }}",
        "subject": "=Reply from {{ $('Incoming text').first().json.body.From }} (replied to a win-back text)",
        "emailType": "text",
        "message": "=They wrote: {{ $('Incoming text').first().json.body.Body }}\n\nReply to them from your phone or the Twilio console.",
        "options": {}
      },
      "credentials": {
        "gmailOAuth2": {
          "id": "REPLACE_GMAIL_CREDENTIAL",
          "name": "Gmail account (placeholder)"
        }
      }
    },
    {
      "id": "7dd0a173-b85e-53f1-ac60-df598d637347",
      "name": "Setup steps",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        -560,
        -200
      ],
      "parameters": {
        "content": "## Setup (about 1-2 hours)\n1. Make a Google Sheet with a tab named `Quotes` and columns `name | phone | last_visit | last_texted | opted_out`. Phones in +1 format, dates as YYYY-MM-DD (format the columns as plain text). List open quotes from people who agreed to texts. Use the date the quote went out as last_visit, and delete the row once they buy or say no.\n2. Fill in **Config (edit me)**: sheet ID, how many days counts as overdue (**winback_days**), the daily cap, Twilio number, **auth token** and webhook URL.\n3. Connect **Google Sheets**, **Twilio** and **Gmail** credentials.\n4. In Twilio > Phone Numbers > your number > Messaging: 'A message comes in' > Webhook, HTTP POST, `https://YOUR-N8N-URL/webhook/win-back-sms`. Paste the exact same URL into **twilio_sms_webhook_url** in Config. A number has one 'message comes in' webhook, so if you run several of these workflows on one number, point it at only one of them.\n5. Only include people who agreed to get texts from you. Activate: every morning at 10 it texts up to the daily cap, once per visit.\n\n**Security:** the Check Twilio signature node rejects (403) anything not signed with your Twilio auth token. Self-hosted n8n: set `NODE_FUNCTION_ALLOW_BUILTIN=crypto`. Don't share this workflow once your token is in it.",
        "height": 640,
        "width": 540
      }
    }
  ],
  "connections": {
    "Every morning": {
      "main": [
        [
          {
            "node": "Config (edit me)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Incoming text": {
      "main": [
        [
          {
            "node": "Config (edit me)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Config (edit me)": {
      "main": [
        [
          {
            "node": "Scheduled run or text?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Scheduled run or text?": {
      "main": [
        [
          {
            "node": "Read your list",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Check Twilio signature",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Read your list": {
      "main": [
        [
          {
            "node": "Due for a nudge?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Due for a nudge?": {
      "main": [
        [
          {
            "node": "Daily cap",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Daily cap": {
      "main": [
        [
          {
            "node": "Send the win-back text",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Send the win-back text": {
      "main": [
        [
          {
            "node": "Mark as texted",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Mark as texted": {
      "main": [
        [
          {
            "node": "Email you a summary",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check Twilio signature": {
      "main": [
        [
          {
            "node": "Signature valid?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Signature valid?": {
      "main": [
        [
          {
            "node": "Acknowledge Twilio (text)",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Reject: not from Twilio (403)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Acknowledge Twilio (text)": {
      "main": [
        [
          {
            "node": "Says URGENT?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Says URGENT?": {
      "main": [
        [
          {
            "node": "Text the owner (URGENT)",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Forward the reply to you",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Text the owner (URGENT)": {
      "main": [
        [
          {
            "node": "Text the customer (URGENT ack)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Text the customer (URGENT ack)": {
      "main": [
        [
          {
            "node": "Email the owner (URGENT, optional)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "active": false,
  "settings": {
    "executionOrder": "v1"
  },
  "pinData": {},
  "meta": {
    "templateCredsSetupCompleted": false,
    "generatedBy": "Speed-to-Lead Playbooks"
  },
  "tags": []
}